自定义 JsonConverter WriteJson 不会改变子属性的序列化

Posted

技术标签:

【中文标题】自定义 JsonConverter WriteJson 不会改变子属性的序列化【英文标题】:Custom JsonConverter WriteJson Does Not Alter Serialization of Sub-properties 【发布时间】:2015-05-29 19:04:21 【问题描述】:

我一直觉得 JSON 序列化程序实际上会遍历整个对象的树,并在遇到的每个接口类型对象上执行自定义 JsonConverter 的 WriteJson 函数 - 并非如此。

我有以下类和接口:

public interface IAnimal

    string Name  get; set; 
    string Speak();
    List<IAnimal> Children  get; set; 


public class Cat : IAnimal

    public string Name  get; set; 
    public List<IAnimal> Children  get; set;         

    public Cat()
    
        Children = new List<IAnimal>();
    

    public Cat(string name="") : this()
    
        Name = name;
    

    public string Speak()
    
        return "Meow";
           


 public class Dog : IAnimal
 
    public string Name  get; set; 
    public List<IAnimal> Children  get; set; 

    public Dog()
    
        Children = new List<IAnimal>();   
    

    public Dog(string name="") : this()
    
        Name = name;
    

    public string Speak()
    
        return "Arf";
    


为了避免 JSON 中的 $type 属性,我写了一个自定义的 JsonConverter 类,它的 WriteJson 是

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

    JToken t = JToken.FromObject(value);

    if (t.Type != JTokenType.Object)
    
        t.WriteTo(writer);                
    
    else
    
        IAnimal animal = value as IAnimal;
        JObject o = (JObject)t;

        if (animal != null)
        
            if (animal is Dog)
            
                o.AddFirst(new JProperty("type", "Dog"));
                //o.Find
            
            else if (animal is Cat)
            
                o.AddFirst(new JProperty("type", "Cat"));
            

            foreach(IAnimal childAnimal in animal.Children)
            
                // ???
            

            o.WriteTo(writer);
        
    

在这个例子中,是的,狗可以为孩子养猫,反之亦然。在转换器中,我想插入“类型”属性,以便将其保存到序列化中。我有以下设置。 (Zoo 只有一个名称和一个 IAnimals 列表。为了简洁和懒惰,我没有在此处包含它;))

Zoo hardcodedZoo = new Zoo()
               Name = "My Zoo",               
                Animals = new List<IAnimal>  new Dog("Ruff"), new Cat("Cleo"),
                    new Dog("Rover")
                        Children = new List<IAnimal> new Dog("Fido"), new Dog("Fluffy")
                     
            ;

            JsonSerializerSettings settings = new JsonSerializerSettings()
                ContractResolver = new CamelCasePropertyNamesContractResolver() ,                    
                Formatting = Formatting.Indented
            ;
            settings.Converters.Add(new AnimalsConverter());            

            string serializedHardCodedZoo = JsonConvert.SerializeObject(hardcodedZoo, settings);

serializedHardCodedZoo序列化后有如下输出:


  "name": "My Zoo",
  "animals": [
    
      "type": "Dog",
      "Name": "Ruff",
      "Children": []
    ,
    
      "type": "Cat",
      "Name": "Cleo",
      "Children": []
    ,
    
      "type": "Dog",
      "Name": "Rover",
      "Children": [
        
          "Name": "Fido",
          "Children": []
        ,
        
          "Name": "Fluffy",
          "Children": []
        
      ]
    
  ]

type 属性显示在 Ruff、Cleo 和 Rover 上,但不显示在 Fido 和 Fluffy 上。我猜 WriteJson 不是递归调用的。我如何在那里获得该类型的属性?

顺便说一句,为什么 IAnimals 不像我期望的那样是驼峰式的?

【问题讨论】:

【参考方案1】:

您的转换器未应用于您的子对象的原因是JToken.FromObject() 在内部使用了一个新的序列化程序实例,它不知道您的转换器。有一个重载允许您传入序列化程序,但如果在这里这样做,您将遇到另一个问题:因为您在转换器内部并且您正在使用JToken.FromObject() 尝试序列化父对象,您将进入无限递归循环。 (JToken.FromObject() 调用序列化器,它调用你的转换器,它调用JToken.FromObject(),等等)

要解决此问题,您必须手动处理父对象。您可以使用一些反射来枚举父属性,而不会遇到太多麻烦:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

    JObject jo = new JObject();
    Type type = value.GetType();
    jo.Add("type", type.Name);

    foreach (PropertyInfo prop in type.GetProperties())
    
        if (prop.CanRead)
        
            object propVal = prop.GetValue(value, null);
            if (propVal != null)
            
                jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
            
        
    
    jo.WriteTo(writer);

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

【讨论】:

值得注意的是,这可能会导致其他基于序列化的属性被忽略。例如,如果你把 [JsonIgnore] 放在一个属性上,它仍然会被上面的代码添加。 这里有类似的问题***.com/questions/43821065/…你能帮忙吗? 有什么更新可以做到这一点,尊重这些属性吗?【参考方案2】:

这是一个想法,而不是对每个属性进行反射,而是遍历通常序列化的 JObject,然后更改您感兴趣的属性的标记。

这样您仍然可以利用所有“JsonIgnore”属性和其他内置的有吸引力的功能。

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

    JToken jToken = JToken.FromObject(value);

    if (jToken.Type == JTokenType.Object)
    
        JObject jObject = (JObject)jToken;
        ...
        AddRemoveSerializedProperties(jObject, val);
        ...
    
    ...

然后

private void AddRemoveSerializedProperties(JObject jObject, MahMan baseContract)
   
       jObject.AddFirst(....);

        foreach (KeyValuePair<string, JToken> propertyJToken in jObject)
        
            if (propertyJToken.Value.Type != JTokenType.Object)
                continue;

            JToken nestedJObject = propertyJToken.Value;
            PropertyInfo clrProperty = baseContract.GetType().GetProperty(propertyJToken.Key);
            MahMan nestedObjectValue = clrProperty.GetValue(baseContract) as MahMan;
            if(nestedObj != null)
                AddRemoveSerializedProperties((JObject)nestedJObject, nestedObjectValue);
        
    

【讨论】:

jObject 来自哪里? 嗨,李,很抱歉,如果 jToken 类型是“对象”,jToken 可以转换为 jObject 我添加了缺失的行,如果您有任何其他问题戳我【参考方案3】:

我在为父子类型使用两个自定义转换器时遇到了这个问题。我发现一个更简单的方法是,由于JToken.FromObject() 的重载将serializer 作为参数,因此您可以传递WriteJson() 中给出的序列化程序。但是,您需要从序列化程序中删除您的转换器以避免对其进行递归调用(但在之后将其重新添加):

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

    serializer.Converters.Remove(this);
    JToken jToken = JToken.FromObject(value, serializer);
    serializer.Converters.Add(this);

    // Perform any necessary conversions on the object returned

【讨论】:

这是一个不好的做法:1. 它不是威胁安全的 2. 它将无法以与此父节点相同的方式序列化相同类型的子节点(因为缺少当前的序列化程序) 3.如果 FormObject 失败了,你的 seralizer 的状态已经改变 4. 如果它没有失败,那么应用转换器的顺序可能已经改变了。【参考方案4】:

这是解决您的问题的一个 hacky 解决方案,它可以完成工作并且看起来很整洁。

public class MyJsonConverter : JsonConverter

    public const string TypePropertyName = "type";
    private bool _dormant = false;

    /// <summary>
    /// A hack is involved:
    ///     " JToken.FromObject(value, serializer); " creates amn infinite loop in normal circumstances
    ///     for that reason before calling it "_dormant = true;" is called.
    ///     the result is that this JsonConverter will reply false to exactly one "CanConvert()" call.
    ///     this gap will allow to generate a a basic version without any extra properties, and then add them on the call with " JToken.FromObject(value, serializer); ".
    /// </summary>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        _dormant = true;
        JToken t = JToken.FromObject(value, serializer);
        if (t.Type == JTokenType.Object && value is IContent)
        
            JObject o = (JObject)t;
            o.AddFirst(new JProperty(TypePropertyName, value.GetType().Name));
            o.WriteTo(writer);
        
        else
        
            t.WriteTo(writer);
        
    

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

    public override bool CanRead => false;

    public override bool CanConvert(Type objectType)
    
        if (_dormant)
        
            _dormant = false;
            return false;
        
        return true;
    

【讨论】:

以上是关于自定义 JsonConverter WriteJson 不会改变子属性的序列化的主要内容,如果未能解决你的问题,请参考以下文章

通过选项自定义 JsonConverter 的不同输出?

NewtonsoftJson 中的自定义 JSONConverter 用于序列化

自定义 JsonConverter WriteJson 不会改变子属性的序列化

具有数据类型的 Json.NET 自定义 JsonConverter

如何在 JSON.NET 中实现自定义 JsonConverter?

将自定义 JsonConverter 添加到 Web API 会影响传递给自定义验证属性的字符串值