自定义 JsonSchemaValidation 错误消息

Mid*_*hew 7 c# json jsonschema json-schema-validator

我正在使用 Json 架构来验证 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();
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

我正在寻找类似的东西:

 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.
Run Code Online (Sandbox Code Playgroud)

Dai*_*Dai 0

我有类似的问题。

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

第 1 部分:背景:

事实证明,这是因为仅使用数据注释属性的子集JSchemaGenerator生成规则 - 并且它忽略了该属性。JSchemaErrorMessage

它使用的属性集记录在“生成模式”页面上

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
Run Code Online (Sandbox Code Playgroud)

因此,有了这些知识,我们就可以尝试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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

第 4 部分:ErrorMessageSchemaValidationEventArgs

因此,给定您的一组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" );
        }
Run Code Online (Sandbox Code Playgroud)