首页 > 解决方案 > newtonsoft json模式反序列化ValidationError

问题描述

使用 Newtonsoft.Json.Schema。似乎 NewtonsoftJsonConvert无法反序列化自己的ValidationError.

具体来说,以下将失败:

var dataFile = System.IO.File.ReadlAllText("data.json");
var schemaFile = System.IO.File.ReadlAllText("schema.json");
... // parsing the files
model.IsValid(schema, out IList<Newtonsoft.Json.Schema.ValidationError> ve);
var errors = ve as List<Newtonsoft.Json.Schema.ValidationError>; // <-- this may be a problem?
var resultStr = Newtonsoft.Json.JsonConvert.SerializeObject(errors); // <-- this works as expected, though
var ReSerializedResult = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Newtonsoft.Json.Schema.ValidationError>>(resultStr);

序列化后,我得到了 resultStr 的有效字符串,例如

[{\"Message\": \"String 'key' does not match regex pattern ... \", ... }] 

再次反序列化后,我得到一个类型为 Newtonsoft.Json.Schema.ValidationError 的数组(验证结果有一个错误,所以没关系),但它的所有字段都在它们的默认值中。

有没有其他人也遇到过这种情况?有没有办法使用 Json.NET 往返,或者我应该在其GitHub 问题页面ValidationError上打开一个问题?

标签: c#jsonjson.netjsonschema

解决方案


您在这里遇到了几个问题。

首先,ValidationError它是公开不可变的(即所有属性都缺少公共设置器)并且只有一个构造函数,它是非参数化的。因此第三方应用程序(包括 Json.NET 本身)无法填充这种类型的实例。

然而,参考资料显示,大多数属性都有私有设置器。因此,应该可以SisoJsonDefaultContractResolverdaniel这个答案适应 Json.Net中的Private setter到 round-trip 。首先定义:ValidationError

public class SelectedPrivateSettersContractResolver : DefaultContractResolver
{
    HashSet<Type> privateSetterTypes { get; } = new ();
    
    public SelectedPrivateSettersContractResolver(params Type [] types) : this((IEnumerable<Type>)types) { }

    public SelectedPrivateSettersContractResolver(IEnumerable<Type> types) => 
        privateSetterTypes.UnionWith(types ?? throw new ArgumentNullException());

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);

        if (!prop.Ignored && prop.Readable && !prop.Writable)
            if (privateSetterTypes.Contains(prop.DeclaringType))            
                if (member is PropertyInfo property)
                    prop.Writable = property.GetSetMethod(true) != null;

        return prop;
    }       
}

现在你可以这样做:

var settings = new JsonSerializerSettings
{
    ContractResolver = new SelectedPrivateSettersContractResolver(typeof(ValidationError)),
};
var resultStr = JsonConvert.SerializeObject(errors, Formatting.Indented, settings);
var ReSerializedResult = JsonConvert.DeserializeObject<List<ValidationError>>(resultStr, settings);

演示小提琴#1在这里

但是,虽然这允许大多数ValidationError属性成功往返,但Message属性不是。之所以会出现第二个问题,是因为在当前的实现中,Message没有 getter。_message相反,它返回按需计算的字段值。因此,有必要强制序列化_message(和相关字段_extendedMessage)。继承自的自定义合约解析器SelectedPrivateSettersContractResolver可用于执行此操作:

public class ValidationErrorsContractResolver : SelectedPrivateSettersContractResolver
{
    static readonly string [] validationErrorFields = new [] { "_message", "_extendedMessage" };

    public ValidationErrorsContractResolver() : base(typeof(ValidationError)) { }
    
    protected override List<MemberInfo> GetSerializableMembers(Type objectType)
    {
        var list = base.GetSerializableMembers(objectType);
        if (typeof(ValidationError).IsAssignableFrom(objectType))
        {
            foreach (var fieldName in validationErrorFields)
                if (objectType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) is var f && f != null)
                    list.Add(f);
        }
        return list;
    }
    
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        if (prop.DeclaringType == typeof(ValidationError))
        {
            if (validationErrorFields.Contains(prop.UnderlyingName))
            {
                prop.Ignored = false;
                prop.Readable = prop.Writable = true;
            }
        }
        return prop;
    }
    
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);
        if (typeof(ValidationError).IsAssignableFrom(objectType))
        {
            // Ensure _message and _extendedMessage are calculated.
            contract.OnSerializingCallbacks.Add((o, c) => { var m = ((ValidationError)o).Message; });
        }
        return contract;
    }
}

现在,如果您按以下方式往返:

var settings = new JsonSerializerSettings
{
    ContractResolver = new ValidationErrorsContractResolver(),
};
var resultStr = JsonConvert.SerializeObject(errors, Formatting.Indented, settings);
var ReSerializedResult = JsonConvert.DeserializeObject<List<ValidationError>>(resultStr, settings);

消息成功往返。演示小提琴#2在这里

笔记:

  • 使用反射以这种方式强制序列化私有字段是脆弱的,如果 Newtonsoft 改变ValidationError.

  • 您可能希望按照文档中的ValidationErrorsContractResolver建议缓存和重用以获得最佳性能。

  • 您可能会注意到第三个问题,即 的Schema属性ValidationError未序列化或反序列化,因为 Newtonsoft[JsonIgnore]源代码中已明确标记为。我怀疑他们这样做是为了防止序列化的 JSON 变得过于臃肿。如果您想Schema往返,可以强制将其序列化,ValidationErrorsContractResolver.CreateProperty()如下所示:

     protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
     {
         var prop = base.CreateProperty(member, memberSerialization);
         if (prop.DeclaringType == typeof(ValidationError))
         {
             if (validationErrorFields.Contains(prop.UnderlyingName) 
                 || prop.UnderlyingName == "Schema")
             {
                 prop.Ignored = false;
                 prop.Readable = prop.Writable = true;
             }
         }
         return prop;
     }
    

    但是,如果您这样做,您的 JSON 将变得更加臃肿,并且如果您序列化多个验证错误,则该JSchema Schema值将在反序列化期间重复,因为它不是通过引用保存的。

    演示小提琴#3在这里


推荐阅读