【问题标题】:Customize JsonSchemaValidation Error Messages自定义 JsonSchemaValidation 错误消息
【发布时间】:2017-05-23 13:27:13
【问题描述】:

我正在使用 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();

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

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.

【问题讨论】:

  • 我遇到了与此完全相同的问题。如果我找到解决方案,将在此处发布。目前,我已经编写了自己的类,针对我正在使用的特定模式进行了定制,这些类对 Json-Schema-Validator 返回的错误数据执行特定检查,并返回更用户友好的错误消息版本.我正在考虑为此开始使用我自己的可能的开源验证库。

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


【解决方案1】:

我也遇到过类似的问题。

就我而言,我使用的是Newtonsoft.Json.Schema(是的,我支付了许可证费用!)从System.ComponentModel.DataAnnotations 属性生成JSchema - 我看到它生成了正确的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 部分:从 SchemaValidationEventArgs 获取 ErrorMessage

因此,鉴于您的 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" );
        }

【讨论】:

    猜你喜欢
    • 2016-09-03
    • 2015-09-21
    • 2011-04-27
    • 2018-09-02
    • 2021-09-13
    • 2017-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多