自定义JsonSchemaValidation错误消息

时间:2017-05-23 13:27:14

标签: c# json jsonschema json-schema-validator

我正在使用Json Schema来验证json对象。我正确收到错误消息。但它看起来更像是开发人员友好的错误消息。有没有办法自定义错误消息,以便我可以使它更友好。我已经看过许多论坛,我无法找到解决方案。

以下是我使用的代码:

        string Json = @"{'Sheet1':[{'Location':'#$','First Name':11,'Last Name':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA','Amount':'A','Date of Birth':'8522/85/25'}]}";
        string JSONSchemaValidator = @"{'$schema':'http://json-schema.org/draft-04/schema#','title':'JSON Validation Schema','type':'object','additionalProperties':true,'properties':{'Sheet1':{'type':'array','items':{'type':'object','additionalProperties':true,'properties':{'Location':{'type':['number','string','null'],'pattern':'^[a-zA-Z0-9\\-\\s]+$','maxLength':15},'First Name':{'type':['string','null'],'maxLength':20,'pattern':'^[a-zA-Z\\-\\s]+$'},'Last Name':{'type':['string','null'],'maxLength':10,'pattern':'^[a-zA-Z\\-\\s]+$'},'Amount':{'type':['number','null'],'minimum':-999999999.99,'maximum':999999999.99,'exclusiveMaximum':true,'multipleOf':0.01},'Date of Birth':{'type':['string','null'],'format':'date-time'}}}}}}";
        JSchema schema = JSchema.Parse(JSONSchemaValidator);
        JObject person = JObject.Parse(Json);
        IList<string> iJSONSchemaValidatorErrorList;
        bool valid = person.IsValid(schema, out iJSONSchemaValidatorErrorList);

        if (iJSONSchemaValidatorErrorList != null && iJSONSchemaValidatorErrorList.Count > 0)
        {
            foreach (string error in iJSONSchemaValidatorErrorList)
            {
                Console.WriteLine(error);
            }
        }
        Console.ReadKey();

以下是我收到的错误消息:

1. String '#$' does not match regex pattern '^[a-zA-Z0-9\-\s]+$'. Path 'Sheet1[0].Location', line 1, position 27.
2. Invalid type. Expected String, Null but got Integer. Path 'Sheet1[0]['First Name']', line 1, position 43.
3. String 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA' exceeds maximum length of 10. Path 'Sheet1[0]['Last Name']', line 1, position 87.
4. Invalid type. Expected Number, Null but got String. Path 'Sheet1[0].Amount', line 1, position 100.
5. String '8522/85/25' does not validate against format 'date-time'. Path 'Sheet1[0]['Date of Birth']', line 1, position 129.

我正在寻找类似的东西:

 1. 'Location' in Column 1 of Sheet1 should be alphanumeric.
 2. 'Name' in Column 1 of Sheet1 should only contain alphabets.
 3. 'Last Name' in column 1 exceeds maximum length of 10.
 4. 'Amount' in column 1 should contain only numbers.
 5. 'Date of Birth' in column 1 is not a valid date.

1 个答案:

答案 0 :(得分:0)

我有类似的问题。

就我而言,我正在使用Newtonsoft.Json.Schema(是的,我付了许可证费用!)正在根据JSchema属性生成一个System.ComponentModel.DataAnnotations-我看到它正在生成正确的JSchema,但是验证错误消息根本没有在属性上使用ErrorMessage属性。

第1部分:背景:

事实证明,这是因为JSchemaGenerator仅使用Data-Annotation属性的子集生成JSchema规则-而忽略了ErrorMessage属性。

它确实使用are documented on the "Generating schemas" page的一组属性:

JSchema member            Data Annotation attribute class        Attribute properties used
------------------------------------------------------------------------------------------
required                  [Required]                             
minLength                 [StringLength]                         .MinimumLength
maxLength                 [StringLength]                         .MaximumLength
minLength                 [MinLength]                            .Length
maxLength                 [MaxLength]                            .Length
minItems                  [MinLength]                            .Length
maxItems                  [MaxLength]                            .Length
pattern                   [RegularExpression]                    .Pattern
enum                      [EnumDataType]                         .EnumType
format                    [DataType]                             .DataType
format                    [Url]
format                    [Phone]
format                    [EmailAddress]
minimum                   [Range]                                .Minimum
maximum                   [Range]                                .Maximum

因此,有了这些知识,我们可以尝试通过反映要反序列化的类型并在适当的ErrorMessage上查找匹配的数据注释属性,来从属性的PropertyInfo属性中获取自定义的验证消息。成员。


第2部分:ValidationError

来自JSchemaValidatingReader的验证错误显示为Newtonsoft.Json.Schema.ValidationError对象,其中包含:

  • Path属性,我们需要将其与目标类型的属性名称或任何匹配的[JsonProperty]属性相反。
  • ErrorType枚举,我们可以使用它来确定要查找的属性类型。

第3部分:静态JSON属性和数据注释缓存

出于性能方面的考虑,我们不想每次都对每个类型和属性进行反思,因此下面的代码是静态通用缓存(请确保将其标记为internal,以避免违反CA1000)。

internal static class JsonPropertiesCache<T>
{
    /// <summary>Case-sensitive.</summary>
    public static IReadOnlyDictionary<String,JsonPropertyDataAnnotationAttributes> AttributesByJsonPropertyName { get; } = CreateJsonPropertiesDictionary();

    private static Dictionary<String,JsonPropertyDataAnnotationAttributes> CreateJsonPropertiesDictionary()
    {
        return typeof(T)
            .GetProperties( BindingFlags.Instance | BindingFlags.Public )
            .Select( pi => ( ok: JsonPropertyDataAnnotationAttributes.TryCreate( pi, out JsonPropertyDataAnnotationAttributes attrs ), attrs ) )
            .Where( t => t.ok )
            .Select( t => t.attrs )
#if DEBUG
            .ToDictionary( attrs => attrs.Name ); // This will throw if there's a duplicate JSON property name.
#else
            .GroupBy( attrs => attrs.Name )
            .ToDictionary( grp => grp.Key, grp => grp.First() ); // This won't throw, but will only use attributes from the first match (whichever match that is).
#endif
    }
}

public class JsonPropertyDataAnnotationAttributes
{
    public static Boolean TryCreate( PropertyInfo pi, out JsonPropertyDataAnnotationAttributes attrs )
    {
        // Only create an instance if at least one DataAnnotation validation attribute is present:

        if( pi?.GetCustomAttributes<ValidationAttribute>()?.Any() ?? false )
        {
            attrs = new JsonPropertyDataAnnotationAttributes( pi );
            return true;
        }
        else
        {
            attrs = default;
            return false;
        }
    }

    public JsonPropertyDataAnnotationAttributes( PropertyInfo pi )
    {
        this.PropertyInfo      = pi ?? throw new ArgumentNullException( nameof(pi) );
        this.JsonProperty      = pi.GetCustomAttribute<JsonPropertyAttribute>();

        this.Required          = pi.GetCustomAttribute<RequiredAttribute>();
        this.MinLength         = pi.GetCustomAttribute<MinLengthAttribute>();
        this.StringLength      = pi.GetCustomAttribute<StringLengthAttribute>();
        this.MaxLength         = pi.GetCustomAttribute<MaxLengthAttribute>();
        this.RegularExpression = pi.GetCustomAttribute<RegularExpressionAttribute>();
        this.Url               = pi.GetCustomAttribute<UrlAttribute>();
        this.Phone             = pi.GetCustomAttribute<PhoneAttribute>();
        this.Email             = pi.GetCustomAttribute<EmailAddressAttribute>();
        this.Range             = pi.GetCustomAttribute<RangeAttribute>();
        this.EnumDataType      = pi.GetCustomAttribute<EnumDataTypeAttribute>(); // NOTE: `EnumDataTypeAttribute` is a subclass of `DataTypeAttribute`.
        this.DataType          = pi.GetCustomAttribute<DataTypeAttribute>();
    }

    public PropertyInfo               PropertyInfo      { get; }
    public JsonPropertyAttribute      JsonProperty      { get; }
    public String                     Name              => this.JsonProperty?.PropertyName ?? this.PropertyInfo.Name; // TODO: Support custom NamingStrategies.

    public RequiredAttribute          Required          { get; }
    public MinLengthAttribute         MinLength         { get; }
    public StringLengthAttribute      StringLength      { get; }
    public MaxLengthAttribute         MaxLength         { get; }
    public RegularExpressionAttribute RegularExpression { get; }
    public UrlAttribute               Url               { get; }
    public PhoneAttribute             Phone             { get; }
    public EmailAddressAttribute      Email             { get; }
    public RangeAttribute             Range             { get; }
    public EnumDataTypeAttribute      EnumDataType      { get; }
    public DataTypeAttribute          DataType          { get; }

    public Boolean TryFormatValidationErrorMessage( ErrorType errorType, out String errorMessage )
    {
        switch( errorType )
        {
        case ErrorType.MaximumLength:

            errorMessage = this.MaxLength?.FormatErrorMessage( this.Name ) ?? this.StringLength?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.MinimumLength:

            errorMessage = this.MinLength?.FormatErrorMessage( this.Name ) ?? this.StringLength?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.Pattern:

            errorMessage = this.RegularExpression?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );
                
        case ErrorType.MaximumItems:
                
            errorMessage = this.MaxLength?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.MinimumItems:
                
            errorMessage = this.MinLength?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.Required:
                
            errorMessage = this.Required?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.Enum:
                
            errorMessage = this.EnumDataType?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.Format:

            // Same precedence order as used by Newtonsoft.Json.Schema.Infrastructure::GetFormat(JsonProperty):
                errorMessage = this.Url?.FormatErrorMessage( this.Name ) ?? this.Phone?.FormatErrorMessage( this.Name ) ?? this.Email?.FormatErrorMessage( this.Name ) ?? this.DataType?.FormatErrorMessage( this.Name );
                return !String.IsNullOrEmpty( errorMessage );

        case ErrorType.Maximum:
        case ErrorType.Minimum:
                
            errorMessage = this.Range?.FormatErrorMessage( this.Name );
            return !String.IsNullOrEmpty( errorMessage );

        default:
            errorMessage = default;
            return false;
        }
    }
}

第4部分:从ErrorMessage获得SchemaValidationEventArgs

因此,考虑到您拥有SchemaValidationEventArgs的集合,获取与人熟悉的验证错误是匹配您要反序列化的类型并使用上述字典的简单情况。

注意事项:该代码当前不支持本身就是类的DTO类JSON属性-但实现起来非常简单,对读者来说是一个练习。

private static Result<T> DeserializeAndValidate<T>( String body, JSchema schema )
{
    if( body == null ) throw new ArgumentNullException(nameof(body));

    //

    List<SchemaValidationEventArgs> messages = new List<SchemaValidationEventArgs>();

    using( StringReader textReader = new StringReader( body ) )
    using( JsonTextReader jsonTextReader = new JsonTextReader( textReader ) )
    using( JSchemaValidatingReader validatingReader = new JSchemaValidatingReader( jsonTextReader ) )
    {
        validatingReader.Schema = schema;
        validatingReader.ValidationEventHandler += ( sender, eventArgs ) => messages.Add( eventArgs );

        //

        JsonSerializer serializer = new JsonSerializer();

        T value = serializer.Deserialize<T>( validatingReader );
                
        if( messages.Count == 0 )
        {
            return value;
        }
        else
        {
            IReadOnlyDictionary<String,JsonPropertyDataAnnotationAttributes> dict = JsonPropertiesCache<T>.AttributesByJsonPropertyName;

            return messages
                .Select( e => e.ValidationError )
                .Flatten( err => err.ChildErrors )
                .Select( err => ( err, ok: dict.TryGetValue( err.Path, out JsonPropertyDataAnnotationAttributes attr ), attr ) )
                .Select( t => t.ok && t.attr.TryFormatValidationErrorMessage( t.err.ErrorType, out String errorMessage ) ? errorMessage : t.err.Message )
                .StringJoin( separator: "\r\n" );
        }