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

22

我一直无法找到一个合理的实现方式来使用JsonConvert.WriteJson在序列化特定类型时插入JSON属性。所有尝试都导致了"JsonSerializationException : Self referencing loop detected with type XXX"。

关于我正在解决的问题,需要提供更多背景信息:我正在使用JSON作为配置文件格式,并且我正在使用JsonConverter来控制我的配置类型的类型解析、序列化和反序列化。我想使用更有意义的JSON值来解析正确的类型,而不是使用$type属性。

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

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

属性 "Target":"B" 是用于确定该对象应序列化为类型 B 的JSON属性。尽管这个例子很简单,但是该设计可以使配置文件格式更易用。

我也希望配置文件可以进行往返转换。我已经解决了反序列化的问题,但我没有解决序列化的问题。

我的问题根源在于找不到使用标准JSON序列化逻辑且不会抛出“自引用循环”的异常的JsonConverter.WriteJson 实现。以下是我的实现:

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方法编写单个对象和属性。
以下是展示我的问题的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());
    }

}

1
在您的转换器中的WriteJson方法中,您尝试过从JObject.FromObject()调用中完全删除serializer参数吗?在这个fiddle中似乎可以工作。 - Brian Rogers
谢谢Brian - 感谢您查看此问题,并且您是正确的,那样修复了异常。然而,这并没有解决我的问题,因为我需要能够在嵌套对象中执行此操作。我已更新示例以涵盖该内容。或者,请参见https://dotnetfiddle.net/b3yrEU(Fiddle很酷!) - crimbo
1
我很想了解你最终得出的结果。我也遇到了同样的问题。 - Andrew Savinykh
8个回答

16

在转换器内部调用JObject.FromObject()来转换同一对象将导致递归循环,正如你所看到的。通常解决方案是要么(a)在转换器内使用单独的JsonSerializer实例,要么(b)手动序列化属性,就像James在他的答案中指出的那样。但是对于你的情况,这两个解决方案都不太适用:如果你使用一个不知道转换器的单独的序列化程序,则你的子对象将不能得到它们的提示属性。而完全手动序列化则无法提供通用解决方案,正如你在评论中提到的那样。

幸运的是,有一个折衷的办法。您可以在WriteJson方法中使用一些反射来获取对象属性,然后从那里委托给JToken.FromObject()。转换器将按其应该的方式递归调用子属性,但不会针对当前对象进行调用,因此您不会遇到麻烦。这种解决方案的一个注意事项是:如果您碰巧在此转换器处理的类(例如A、B和C)上应用了任何[JsonProperty]属性,则不会尊重这些属性。

以下是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);
}

Fiddle: https://dotnetfiddle.net/jQrxb8


9

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

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

我们必须重写序列化程序,以便我们可以指定一个自定义解析器:
public class ForcedObjectSerializer : JsonSerializer
{
    public ForcedObjectSerializer()
        : base()
    {
        this.ContractResolver = new ForcedObjectResolver();
    }
}

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

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

这应该可以帮到你,或者足够接近了。 :) 我在https://github.com/RoushTech/SegmentDotNet/中使用它,其中有测试用例涵盖了此用例(包括嵌套我们的自定义序列化类),关于此讨论的详细信息请参见:https://github.com/JamesNK/Newtonsoft.Json/issues/386


2
这绝对是这里最被低估的答案。值得一提的是,对我来说这并不是一个100%完美的解决方案,因为我真的想使用原始序列化程序中的所有设置。查看此答案。您可以考虑优化此答案以反映其优点。仍然,非常出色的工作。 - Zachary Dow

3
这样怎么样?
public class TypeHintContractResolver : DefaultContractResolver
{

  protected override IList<JsonProperty> CreateProperties(Type type,
      MemberSerialization memberSerialization)
  {
    IList<JsonProperty> result = base.CreateProperties(type, memberSerialization);
    if (type == typeof(A))
    {
      result.Add(CreateTypeHintProperty(type,"Hint", "A"));
    }
    else if (type == typeof(B))
    {
      result.Add(CreateTypeHintProperty(type,"Target", "B"));
    }
    else if (type == typeof(C))
    {
      result.Add(CreateTypeHintProperty(type,"Is", "C"));
    }
    return result;
  }

  private JsonProperty CreateTypeHintProperty(Type declaringType, string propertyName, string propertyValue)
  {
    return new JsonProperty
    {
        PropertyType = typeof (string),
        DeclaringType = declaringType,
        PropertyName = propertyName,
        ValueProvider = new TypeHintValueProvider(propertyValue),
        Readable = false,
        Writable = true
    };
  }
}

所需的类型值提供程序可以非常简单,如下所示:

public class TypeHintValueProvider : IValueProvider
{

  private readonly string _value;
  public TypeHintValueProvider(string value)
  {
    _value = value;
  }

  public void SetValue(object target, object value)
  {        
  }

  public object GetValue(object target)
  {
    return _value;
  }

}

Fiddle: https://dotnetfiddle.net/DRNzz8


这看起来像是我想要的,但我想用它进行序列化,但我无法使其工作(属性被添加但在浏览器中不显示为JSON)。它应该适用于序列化吗? - Richard Watts

2

Brian的回答非常好,应该能帮助到提问者,但是他的回答有一些问题,其他人也可能会遇到,具体包括:1)在序列化数组属性时会抛出溢出异常,2)任何静态公共属性都将被发射到JSON中,这可能不是你想要的。

下面是另一个版本,解决了这些问题:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    Type valueType = value.GetType();
    if (valueType.IsArray)
    {
        var jArray = new JArray();
        foreach (var item in (IEnumerable)value)
            jArray.Add(JToken.FromObject(item, serializer));

        jArray.WriteTo(writer);
    }
    else
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        var jObj = new JObject();
        if (typeHintProperty != null)
            jo.Add(typeHintProperty);

        foreach (PropertyInfo property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (property.CanRead)
            {
                object propertyValue = property.GetValue(value);
                if (propertyValue != null)
                    jObj.Add(property.Name, JToken.FromObject(propertyValue, serializer));
            }
        }

        jObj.WriteTo(writer);
    }
}

在解决这个问题上工作了2个小时后,你的答案终于帮我解决了问题。稍加调整后,问题得以解决。非常感谢! - Robert Massa

2

我在2019年遇到了这个问题 :)

答案是,如果你不想被@stackoverflow提醒,请记得重写:

  • bool CanWrite
  • bool CanRead

    public class DefaultJsonConverter : JsonConverter
    {
        [ThreadStatic]
        private static bool _isReading;
    
        [ThreadStatic]
        private static bool _isWriting;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            try
            {
                _isWriting = true;
    
                Property typeHintProperty = TypeHintPropertyForType(value.GetType());
    
                var jObject = JObject.FromObject(value, serializer);
                if (typeHintProperty != null)
                {
                    jObject.AddFirst(typeHintProperty);
                }
                writer.WriteToken(jObject.CreateReader());
            }
            finally
            {
                _isWriting = false;
            }
        }
    
        public override bool CanWrite
        {
            get
            {
                if (!_isWriting)
                    return true;
    
                _isWriting = false;
    
                return false;
            }
        }
    
        public override bool CanRead
        {
            get
            {
                if (!_isReading)
                    return true;
    
                _isReading = false;
    
                return false;
            }
        }
    
        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            try
            {
                _isReading = true;
                return serializer.Deserialize(reader, objectType);
            }
            finally
            {
                _isReading = false;
            }
        }
    }
    

感谢: https://github.com/RicoSuter/NJsonSchema/blob/master/src/NJsonSchema/Converters/JsonInheritanceConverter.cs


1
我曾经遇到过类似的问题,以下是我在合同解析器中所做的操作。
if (contract is JsonObjectContract && ShouldUseConverter(type))     
{
    if (contract.Converter is TypeHintJsonConverter)
    {
        contract.Converter = null;
    }
    else
    {
        contract.Converter = new TypeHintJsonConverter(type);
    }
}

这是我找到的唯一避免 StackOverflowException 的方法。实际上,每次调用都不会使用转换器。

0

序列化器正在调用您的转换器,然后转换器又在调用序列化器,接着序列化器再次调用您的转换器,如此循环。

要么使用不包含您的转换器的新序列化器实例来使用 JObject.FromObject,要么手动序列化类型的成员。


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

-2

在遇到相同问题并查找了此及其他类似问题后,我发现 JsonConverter 有一个可重写的属性 CanWrite。

将该属性重写为返回 false,解决了我的问题。

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

希望这能帮助其他遇到同样问题的人。

通过轻微修改 https://dev59.com/B2sy5IYBdhLWcg3w0RXT#9444519 的解决方案,最近我开始从 WriteJson 得到 NotImplementedException,但不知道原因。我怀疑是因为我还重写了 CanConvert。只是想指出一下。 - drzaus
3
如果 CanWrite 返回 false,WriteJson 将不会被调用。 - Andrew Savinykh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接