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



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

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

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

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

其中JSON属性CCD_ 4用于确定该对象应被序列化为类型CCD_。考虑到这个简单的例子,这种设计可能看起来不那么引人注目,但它确实使配置文件格式更可用。

我还希望配置文件是可往返的。我有反序列化的情况,但我不能工作的是序列化的情况。

我的问题的根源是,我找不到一个使用标准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());
    }
}

在转换器内对正在转换的同一对象调用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);
}

Fiddle:https://dotnetfiddle.net/jQrxb8

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

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

这个怎么样:

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

Brian的回答很好,应该有助于OP,但这个回答有几个其他人可能会遇到的问题,即: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);
    }
}

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

我也遇到过类似的问题,下面是我在合同解析程序中所做的操作

if (contract is JsonObjectContract && ShouldUseConverter(type))     
{
    if (contract.Converter is TypeHintJsonConverter)
    {
        contract.Converter = null;
    }
    else
    {
        contract.Converter = new TypeHintJsonConverter(type);
    }
}

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

串行器正在调用您的转换器,然后再调用正在调用转换器的串行器,等等。

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

在遇到同样的问题并找到这个问题和其他类似的问题后,我发现JsonConverter具有可重写属性CanWrite。

覆盖此属性以返回false为我修复了此问题。

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

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

相关内容

  • 没有找到相关文章

最新更新