Json.NET,如何自定义序列化以插入JSON属性

cri*_*mbo 19 c# json.net

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

关于我正在尝试解决的问题的更多背景:我使用JSON作为配置文件格式,我使用a JsonConverter来控制我的配置类型的类型解析,序列化和反序列化.$type我想使用更有意义的JSON值来解决正确的类型,而不是使用属性.

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

{
  "Target": "B",
  "Id": "foo"
}
Run Code Online (Sandbox Code Playgroud)

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

我还希望配置文件是可循环访问的.我有反序列化的工作,我无法工作的是序列化案例.

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

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

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

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

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());
    }

}
Run Code Online (Sandbox Code Playgroud)

Bri*_*ers 14

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

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

以下是该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);
}
Run Code Online (Sandbox Code Playgroud)

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


Str*_*ill 8

使用自定义转换器获取我们忽略的属性的示例,将其分解并将其属性添加到其父对象:

public class ContextBaseSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(ContextBase).GetTypeInfo().IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contextBase = value as ContextBase;
        var valueToken = JToken.FromObject(value, new ForcedObjectSerializer());

        if (contextBase.Properties != null)
        {
            var propertiesToken = JToken.FromObject(contextBase.Properties);
            foreach (var property in propertiesToken.Children<JProperty>())
            {
                valueToken[property.Name] = property.Value;
            }
        }

        valueToken.WriteTo(writer);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们必须覆盖序列化程序,以便我们指定自定义解析器:

public class ForcedObjectSerializer : JsonSerializer
{
    public ForcedObjectSerializer()
        : base()
    {
        this.ContractResolver = new ForcedObjectResolver();
    }
}
Run Code Online (Sandbox Code Playgroud)

在自定义解析器中,我们将从JsonContract中删除转换器,这将强制内部序列化程序使用默认对象序列化程序:

public class ForcedObjectResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        // We're going to null the converter to force it to serialize this as a plain object.
        var contract =  base.ResolveContract(type);
        contract.Converter = null;
        return contract;
    }
}
Run Code Online (Sandbox Code Playgroud)

这应该会让你在那里,或足够接近.:)我在https://github.com/RoushTech/SegmentDotNet/中使用它,其中包含覆盖此用例的测试用例(包括嵌套我们的自定义序列化类),详细讨论内容如下:https://github.com /JamesNK/Newtonsoft.Json/issues/386

  • 这很容易成为这里最被低估的答案.对于它的价值,这对我来说不是100%完美的解决方案,因为我真的想要使用原始序列化器的所有设置.[看看这个答案](http://stackoverflow.com/a/38230327/2283050).您可以考虑改进此答案以反映其益处.仍然,惊人的工作. (2认同)

Jam*_*ing 0

序列化器正在调用您的转换器,然后转换器再调用正在调用您的转换器的序列化器,等等。

使用没有 JObject.FromObject 转换器的序列化程序的新实例,或者手动序列化类型的成员。

  • 谢谢。手动序列化我的类型的成员是行不通的,因为这是一个普遍的问题,我需要它与任何配置的类型一起工作。我正在寻找的是一种拦截正常序列化逻辑以插入属性的方法。理想情况下,它还会使用原始序列化器中的任何其他自定义序列化设置,但我现在可以不用它。我将使用第二个序列化器和 JObject 进行尝试。 (2认同)