Json.NET,如何自定义序列化以插入JSON属性 [英] Json.NET, how to customize serialization to insert a JSON property

查看:82
本文介绍了Json.NET,如何自定义序列化以插入JSON属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我无法找到JsonConvert.WriteJson的合理实现,该实现允许我在序列化特定类型时插入JSON属性.我所有的尝试都导致了"JsonSerializationException:检测到类型为XXX的自引用循环".

I have been unable to find a reasonable implementation for JsonConvert.WriteJson that allows me to insert a JSON property when serializing specific types. All my attempts have resulted in "JsonSerializationException : Self referencing loop detected with type XXX".

我要解决的问题的一些背景知识:我正在使用JSON作为配置文件格式,并且正在使用JsonConverter来控制我的配置类型的类型解析,序列化和反序列化.我想使用更有意义的JSON值来解析正确的类型,而不是使用$type属性.

A little more background on the problem I'm trying to solve: I am using JSON as a config file format, and I'm using a JsonConverter to control the type resolution, serialization, and deserialization of my configuration types. Instead of using the $type property, I want to use more meaningful JSON values that are used to resolve the correct types.

在我的精简示例中,这是一些JSON文本:

In my pared-down example, here's some JSON text:

{
  "Target": "B",
  "Id": "foo"
}

,其中JSON属性"Target": "B"用于确定该对象应序列化为B类型.举这个简单的例子,这种设计似乎并不引人注目,但它确实使配置文件格式更有用.

where the JSON property "Target": "B" is used to determine that this object should be serialized into type B. This design might not seem that compelling given the simple example, but it does make the config file format more usable.

我还希望配置文件是可双向访问的.我有反序列化的情况下工作,我不能工作的是序列化的情况下.

I also want the config files to be round-trippable. I have the deserialize case working, what I can't get working is the serialize case.

我的问题的根源是我找不到使用标准JSON序列化逻辑的JsonConverter.WriteJson实现,并且没有引发自我引用循环"异常.这是我的实现:

The root of my problem is that I can't find an implementation of JsonConverter.WriteJson that uses the standard JSON serialization logic, and doesn't throw a "Self referencing loop" exception. Here's my implementation:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
    // Same error occurs whether I use the serializer parameter or a separate serializer.
    JObject jo = JObject.FromObject(value, serializer); 
    if (typeHintProperty != null)
    {
        jo.AddFirst(typeHintProperty);
    }
    writer.WriteToken(jo.CreateReader());
}

在我看来,这似乎是Json.NET中的错误,因为应该有一种方法可以做到这一点.不幸的是,我遇到的所有JsonConverter.WriteJson示例(例如在JSON.NET中对特定对象的自定义转换)仅提供特定类的自定义序列化,使用JsonWriter方法写出单个对象和属性.

The seems to me to be a bug in Json.NET, because there should be a way to do this. Unfortunately all the examples of JsonConverter.WriteJson that I've come across (eg Custom conversion of specific objects in JSON.NET) only provide custom serialization of a specific class, using the JsonWriter methods to write out individual objects and properties.

这是展示我问题的xunit测试的完整代码(或在此处查看)

Here's the complete code for an xunit test that exhibits my problem (or see it here )

using System;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

using Xunit;


public class A
{
    public string Id { get; set; }
    public A Child { get; set; }
}

public class B : A {}

public class C : A {}

/// <summary>
/// Shows the problem I'm having serializing classes with Json.
/// </summary>
public sealed class JsonTypeConverterProblem
{
    [Fact]
    public void ShowSerializationBug()
    {
        A a = new B()
              {
                  Id = "foo",
                  Child = new C() { Id = "bar" }
              };

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings);
        Console.WriteLine(json);

        Assert.Contains(@"""Target"": ""B""", json);
        Assert.Contains(@"""Is"": ""C""", json);
    }

    [Fact]
    public void DeserializationWorks()
    {
        string json =
@"{
  ""Target"": ""B"",
  ""Id"": ""foo"",
  ""Child"": { 
        ""Is"": ""C"",
        ""Id"": ""bar"",
    }
}";

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        A a = JsonConvert.DeserializeObject<A>(json, jsonSettings);

        Assert.IsType<B>(a);
        Assert.IsType<C>(a.Child);
    }
}

public class TypeHintContractResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        JsonContract contract = base.ResolveContract(type);
        if ((contract is JsonObjectContract)
            && ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types
        {
            contract.Converter = new TypeHintJsonConverter(type);
        }
        return contract;
    }
}


public class TypeHintJsonConverter : JsonConverter
{
    private readonly Type _declaredType;

    public TypeHintJsonConverter(Type declaredType)
    {
        _declaredType = declaredType;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == _declaredType;
    }

    // The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint.
    // TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other.

    private Type TypeFromTypeHint(JObject jo)
    {
        if (new JValue("B").Equals(jo["Target"]))
        {
            return typeof(B);
        }
        else if (new JValue("A").Equals(jo["Hint"]))
        {
            return typeof(A);
        }
        else if (new JValue("C").Equals(jo["Is"]))
        {
            return typeof(C);
        }
        else
        {
            throw new ArgumentException("Type not recognized from JSON");
        }
    }

    private JProperty TypeHintPropertyForType(Type type)
    {
        if (type == typeof(A))
        {
            return new JProperty("Hint", "A");
        }
        else if (type == typeof(B))
        {
            return new JProperty("Target", "B");
        }
        else if (type == typeof(C))
        {
            return new JProperty("Is", "C");
        }
        else
        {
            return null;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (! CanConvert(objectType))
        {
            throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType);
        }

        // Load JObject from stream.  Turns out we're also called for null arrays of our objects,
        // so handle a null by returning one.
        var jToken = JToken.Load(reader);
        if (jToken.Type == JTokenType.Null)
            return null;
        if (jToken.Type != JTokenType.Object)
        {
            throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type);
        }
        JObject jObject = (JObject) jToken;

        // Select the declaredType based on TypeHint
        Type deserializingType = TypeFromTypeHint(jObject);

        var target = Activator.CreateInstance(deserializingType);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
        // Same error occurs whether I use the serializer parameter or a separate serializer.
        JObject jo = JObject.FromObject(value, serializer); 
        if (typeHintProperty != null)
        {
            jo.AddFirst(typeHintProperty);
        }
        writer.WriteToken(jo.CreateReader());
    }

}

如您所见,

推荐答案

在要转换的同一对象上的转换器中调用JObject.FromObject()将导致递归循环.通常,解决方案是(a)在转换器内部使用单独的JsonSerializer实例,或(b)手动序列化属性,如James在他的回答中指出的那样.您的情况有点特殊,因为这些解决方案都不适合您:如果您使用不了解转换器的单独的序列化器实例,则子对象将不会应用其提示属性.正如您在评论中提到的那样,完全手动序列化不适用于通用解决方案.

Calling JObject.FromObject() from within a converter on the same object being converted will result in a recursive loop, as you have seen. Normally the solution is to either (a) use a separate JsonSerializer instance inside the converter, or (b) serialize the properties manually, as James pointed out in his answer. Your case is a little special in that neither of these solutions really work for you: if you use a separate serializer instance that doesn't know about the converter then your child objects will not get their hint properties applied. And serializing completely manually doesn't work for a generalized solution, as you mentioned in your comments.

幸运的是,有一个中间立场.您可以在WriteJson方法中使用一些反射来获取对象属性,然后从那里委托给JToken.FromObject().将对子属性(而不是当前对象)进行递归调用转换器,因此不会麻烦.此解决方案的一个警告:如果碰巧将任何[JsonProperty]属性应用于此转换器处理的类(在您的示例中为A,B和C),则这些属性将不受尊重.

Fortunately, there is a middle ground. You can use a bit of reflection in your WriteJson method to get the object properties, then delegate from there to JToken.FromObject(). The converter will be called recursively as it should for the child properties, but not for the current object, so you don't get into trouble. One caveat with this solution: if you happen to have any [JsonProperty] attributes applied to the classes handled by this converter (A, B and C in your example), those attributes will not be respected.

这是WriteJson方法的更新代码:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    JObject jo = new JObject();
    if (typeHintProperty != null)
    {
        jo.Add(typeHintProperty);
    }
    foreach (PropertyInfo prop in value.GetType().GetProperties())
    {
        if (prop.CanRead)
        {
            object propValue = prop.GetValue(value);
            if (propValue != null)
            {
                jo.Add(prop.Name, JToken.FromObject(propValue, serializer));
            }
        }
    }
    jo.WriteTo(writer);
}

提琴: https://dotnetfiddle.net/jQrxb8

这篇关于Json.NET,如何自定义序列化以插入JSON属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆