JSON.Net 在使用 [JsonConvert()] 时抛出 ***Exception

Posted

技术标签:

【中文标题】JSON.Net 在使用 [JsonConvert()] 时抛出 ***Exception【英文标题】:JSON.Net throws ***Exception when using [JsonConvert()] 【发布时间】:2015-06-25 12:05:04 【问题描述】:

我编写了这个简单的代码来将类序列化为扁平化,但是当我使用[JsonConverter(typeof(FJson))] 注释时,它会抛出一个***Exception。如果我手动调用SerializeObject,它可以正常工作。

如何在 Annotation 模式下使用 JsonConvert:

class Program
    
        static void Main(string[] args)
        
            A a = new A();
            a.id = 1;
            a.b.name = "value";

            string json = null;

            // json = JsonConvert.SerializeObject(a, new FJson()); without [JsonConverter(typeof(FJson))] annotation workd fine
            // json = JsonConvert.SerializeObject(a); ***Exception

            Console.WriteLine(json);
            Console.ReadLine();
        
    

    //[JsonConverter(typeof(FJson))] ***Exception
    public class A
    
        public A()
        
            this.b = new B();
        

        public int id  get; set; 
        public string name  get; set; 
        public B b  get; set; 
    

    public class B
    
        public string name  get; set; 
    

    public class FJson : JsonConverter
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        
            JToken t = JToken.FromObject(value);
            if (t.Type != JTokenType.Object)
            
                t.WriteTo(writer);
                return;
            

            JObject o = (JObject)t;
            writer.WriteStartObject();
            WriteJson(writer, o);
            writer.WriteEndObject();
        

        private void WriteJson(JsonWriter writer, JObject value)
        
            foreach (var p in value.Properties())
            
                if (p.Value is JObject)
                    WriteJson(writer, (JObject)p.Value);
                else
                    p.WriteTo(writer);
            
        

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

        public override bool CanConvert(Type objectType)
        
            return true; // works for any type
        
    

【问题讨论】:

【参考方案1】:

Json.NET 对调用 JToken.FromObject 以生成“默认”序列化然后修改生成的 JToken 以进行输出的转换器没有方便的支持 - 正是因为 ***Exception 由于递归调用 JsonConverter.WriteJson()你观察到的事情会发生。

一种解决方法是使用线程静态布尔值在递归调用中临时禁用转换器。使用线程静态是因为在某些情况下,包括asp.net-web-api,JSON 转换器的实例将在线程之间共享。在这种情况下,通过实例属性禁用转换器将不是线程安全的。

public class FJson : JsonConverter

    [ThreadStatic]
    static bool disabled;

    // Disables the converter in a thread-safe manner.
    bool Disabled  get  return disabled;  set  disabled = value;  

    public override bool CanWrite  get  return !Disabled;  

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        JToken t;
        using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
        
            t = JToken.FromObject(value, serializer);
        

        if (t.Type != JTokenType.Object)
        
            t.WriteTo(writer);
            return;
        

        JObject o = (JObject)t;
        writer.WriteStartObject();
        WriteJson(writer, o);
        writer.WriteEndObject();
    

    private void WriteJson(JsonWriter writer, JObject value)
    
        foreach (var p in value.Properties())
        
            if (p.Value is JObject)
                WriteJson(writer, (JObject)p.Value);
            else
                p.WriteTo(writer);
        
    

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

    public override bool CanConvert(Type objectType)
    
        return true; // works for any type
    


public struct PushValue<T> : IDisposable

    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    

    #region IDisposable Members

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    
        if (setValue != null)
            setValue(oldValue);
    

    #endregion

完成此操作后,您可以将[JsonConverter(typeof(FJson))] 恢复到您的班级A

[JsonConverter(typeof(FJson))]
public class A


演示小提琴 #1 here.

第二种更简单的解决方法为应用了JsonConverter 的类型生成默认的JToken 表示,它利用了应用于成员的转换器取代这一事实应用于类型或设置中的转换器。来自docs:

使用JsonConverter的优先级是成员上的属性定义的JsonConverter,然后是类上的属性定义的JsonConverter,最后是传递给JsonSerializer的任何转换器。

因此,可以通过将其嵌套在 DTO 中为您的类型生成默认序列化,该成员的值是您的类型的实例并应用了 dummy converter读写的默认序列化。

以下扩展方法和转换器可以完成这项工作:

public static partial class JsonExtensions

    public static JToken DefaultFromObject(this JsonSerializer serializer, object value)
    
        if (value == null)
            return JValue.CreateNull();
        var dto = Activator.CreateInstance(typeof(DefaultSerializationDTO<>).MakeGenericType(value.GetType()), value);
        var root = JObject.FromObject(dto, serializer);
        return root["Value"].RemoveFromLowestPossibleParent() ?? JValue.CreateNull();
    

    public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
    
        var oldParent = token.Parent;
    
        var dtoToken = new JObject(new JProperty("Value", token));
        var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
        var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);
        
        if (oldParent == null)
            token.RemoveFromLowestPossibleParent();
            
        return dto == null ? null : dto.GetValue();
    
    
    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    
        if (node == null)
            return null;
        // If the parent is a JProperty, remove that instead of the token itself.
        var contained = node.Parent is JProperty ? node.Parent : node;
        contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (contained is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    
    
    interface IHasValue
    
        object GetValue();
    

    [JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy), IsReference = false)]
    class DefaultSerializationDTO<T> : IHasValue
    
        public DefaultSerializationDTO(T value)  this.Value = value; 

        public DefaultSerializationDTO()  
        
        [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
        public T Value  get; set; 
        
        object IHasValue.GetValue()  return Value; 
    


public class NoConverter : JsonConverter

    // NoConverter taken from this answer https://***.com/a/39739105/3744182
    // To https://***.com/questions/39738714/selectively-use-default-json-converter
    // By https://***.com/users/3744182/dbc
    public override bool CanConvert(Type objectType)   throw new NotImplementedException(); /* This converter should only be applied via attributes */ 

    public override bool CanRead  get  return false;  

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

    public override bool CanWrite  get  return false;  

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)  throw new NotImplementedException(); 

然后在FJson.WriteJson()中使用如下:

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

    JToken t = serializer.DefaultFromObject(value);

    // Remainder as before
    if (t.Type != JTokenType.Object)
    
        t.WriteTo(writer);
        return;
    

    JObject o = (JObject)t;
    writer.WriteStartObject();
    WriteJson(writer, o);
    writer.WriteEndObject();

这种方法的优缺点是:

    它不依赖于递归禁用转换器,因此可以与递归数据模型一起正常工作。

    它不需要重新实现从属性序列化对象的整个逻辑。

    它对中间JToken 表示进行序列化和反序列化。当尝试直接将默认序列化流式传输到传入的JsonReaderJsonWriter 时,不适合使用它。

演示小提琴#2 here.

备注

两个转换器版本都只处理写入;读取未实现。

要解决de序列化过程中的等效问题,请参见例如Json.NET custom serialization with JsonConverter - how to get the "default" behavior.

您编写的转换器会创建具有重复名称的 JSON:


  "id": 1,
  "name": null,
  "name": "value"

这虽然不是严格违法的,但通常被认为是bad practice,因此应该避免。

【讨论】:

【参考方案2】:

在阅读(和测试)Paul Kiar 和 p.kaneman 解决方案之后,我想说实施 WriteJson 似乎是一项具有挑战性的任务。即使它适用于大多数情况 - 仍有一些边缘情况尚未涵盖。 例子:

public bool ShouldSerialize*() 方法 null 价值观 值类型 (struct) json 转换器属性 ..

这是(只是)另一个尝试:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    if (ReferenceEquals(value, null)) 
        writer.WriteNull();
        return;
    

    var contract = (JsonObjectContract)serializer
        .ContractResolver
        .ResolveContract(value.GetType());

    writer.WriteStartObject();

    foreach (var property in contract.Properties) 
        if (property.Ignored) continue;
        if (!ShouldSerialize(property, value)) continue;

        var property_name = property.PropertyName;
        var property_value = property.ValueProvider.GetValue(value);

        writer.WritePropertyName(property_name);
        if (property.Converter != null && property.Converter.CanWrite) 
            property.Converter.WriteJson(writer, property_value, serializer);
         else 
            serializer.Serialize(writer, property_value);
        
    

    writer.WriteEndObject();


private static bool ShouldSerialize(JsonProperty property, object instance) 
    return property.ShouldSerialize == null 
        || property.ShouldSerialize(instance);

【讨论】:

+1 一个不被接受且有 0 票赞成的答案通常不会引起注意,但我想指出,这是最先进的解决方案,让我更接近我想要的位置(在序列化某些属性时,我还需要忽略空值,但有了这个之后就很容易了)。谢谢【参考方案3】:

我不喜欢上面发布的解决方案,所以我弄清楚了序列化程序实际上是如何序列化对象的,并试图将其提炼到最低限度:

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

   JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract( value.GetType() );

   writer.WriteStartObject();
   foreach ( var property in contract.Properties )
   
      writer.WritePropertyName( property.PropertyName );
      writer.WriteValue( property.ValueProvider.GetValue(value));
   
   writer.WriteEndObject();

没有堆栈溢出问题,也不需要递归禁用标志。

【讨论】:

【参考方案4】:

我还不能发表评论,很抱歉……但我只是想为 Paul Kiar 提供的解决方案添加一些内容。他的解决方案真的帮助了我。

Paul 的代码很短,无需任何自定义对象构建即可轻松运行。 我想做的唯一补充是插入一个检查是否忽略该属性。如果设置为忽略,则跳过该属性的写入:

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());

        writer.WriteStartObject();
        foreach (var property in contract.Properties)
        
            if (property.Ignored)
                continue;

            writer.WritePropertyName(property.PropertyName);
            writer.WriteValue(property.ValueProvider.GetValue(value));
        
        writer.WriteEndObject();
    

【讨论】:

【参考方案5】:

通过将属性放在类 A 上,它被递归调用。 WriteJson override 中的第一行再次调用 A 类上的序列化程序。

JToken t = JToken.FromObject(value);

这会导致递归调用,从而导致 ***Exception。

从您的代码中,我认为您正在尝试扁平化层次结构。您可以通过将转换器属性放在属性 B 上来实现这一点,这将避免递归。

//remove the converter from here
public class A

    public A()
    
        this.b = new B();
    

    public int id  get; set; 
    public string name  get; set; 
    [JsonConverter(typeof(FJson))] 
    public B b  get; set; 

警告:您在此处获得的 Json 将有两个名为“name”的键,一个来自 A 类,另一个来自 B 类。

【讨论】:

以上是关于JSON.Net 在使用 [JsonConvert()] 时抛出 ***Exception的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Json.Net 中的 JsonConvert DeserializeObject 之后获取所有不存在的键?

检测反序列化的对象是不是缺少 Json.NET 中 JsonConvert 类的字段

C# 使用Json.NET对数据进行序列化和反序列化 | c# json serialize and deserialize using json.net JsonConvert

如何使用 JsonConvert 从包含空数组的 JSON 字符串中获取 DataTable?

JSON.NET:如何在没有数组的情况下从DataTable对象中仅序列化一行?

Json/XML序列化和反序列化