有时是数组有时是对象时反序列化JSON

Posted

技术标签:

【中文标题】有时是数组有时是对象时反序列化JSON【英文标题】:Deserializing JSON when sometimes array and sometimes object 【发布时间】:2011-07-10 15:06:03 【问题描述】:

我在使用 JSON.NET 库反序列化从 Facebook 返回的数据时遇到了一些问题。

从一个简单的墙贴返回的 JSON 如下所示:


    "attachment":"description":"",
    "permalink":"http://www.facebook.com/permalink.php?story_fbid=123456789"

为照片返回的 JSON 如下所示:

"attachment":
        "media":[
            
                "href":"http://www.facebook.com/photo.php?fbid=12345",
                "alt":"",
                "type":"photo",
                "src":"http://photos-b.ak.fbcdn.net/hphotos-ak-ash1/12345_s.jpg",
                "photo":"aid":"1234","pid":"1234","fbid":"1234","owner":"1234","index":"12","width":"720","height":"482"
        ],

一切都很好,我没有问题。我现在遇到了一个来自移动客户端的简单墙贴,其中包含以下 JSON,而反序列化现在因这一个帖子而失败:

"attachment":
    
        "media":,
        "name":"",
        "caption":"",
        "description":"",
        "properties":,
        "icon":"http://www.facebook.com/images/icons/mobile_app.gif",
        "fb_object_type":""
    ,
"permalink":"http://www.facebook.com/1234"

这是我反序列化的类:

public class FacebookAttachment
    
        public string Name  get; set; 
        public string Description  get; set; 
        public string Href  get; set; 
        public FacebookPostType Fb_Object_Type  get; set; 
        public string Fb_Object_Id  get; set; 

        [JsonConverter(typeof(FacebookMediaJsonConverter))]
        public List<FacebookMedia>  get; set; 

        public string Permalink  get; set; 
    

如果不使用 FacebookMediaJsonConverter,我会收到错误消息:无法将 JSON 对象反序列化为类型“System.Collections.Generic.List`1[FacebookMedia]”。 这是有道理的,因为在 JSON 中,Media 不是一个集合。

我发现这篇文章描述了一个类似的问题,所以我尝试沿着这条路线走:Deserialize JSON, sometimes value is an array, sometimes "" (blank string)

我的转换器看起来像:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

     if (reader.TokenType == JsonToken.StartArray)
          return serializer.Deserialize<List<FacebookMedia>>(reader);
     else
          return null;

效果很好,只是我现在遇到了一个新异常:

在 JsonSerializerInternalReader.cs 内部,CreateValueInternal():反序列化对象时出现意外令牌:PropertyName

reader.Value 的值是“永久链接”。我可以在 switch 中清楚地看到 JsonToken.PropertyName 没有案例。

我需要在转换器中做一些不同的事情吗?谢谢你的帮助。

【问题讨论】:

这能回答你的问题吗? How to handle both a single item and an array for the same property using JSON.net 【参考方案1】:

"Using a Custom JsonConverter to fix bad JSON results" 提供了有关如何处理此案例的非常详细的说明。

总而言之,你可以扩展默认的 JSON.NET 转换器做

    用问题注释属性

    [JsonConverter(typeof(SingleValueArrayConverter<OrderItem>))]
    public List<OrderItem> items;
    

    扩展转换器以返回所需类型的列表,即使是单个对象

    public class SingleValueArrayConverter<T> : JsonConverter
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        
            throw new NotImplementedException();
        
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        
            object retVal = new Object();
            if (reader.TokenType == JsonToken.StartObject)
            
                T instance = (T)serializer.Deserialize(reader, typeof(T));
                retVal = new List<T>()  instance ;
             else if (reader.TokenType == JsonToken.StartArray) 
                retVal = serializer.Deserialize(reader, objectType);
            
            return retVal;
        
    
        public override bool CanConvert(Type objectType)
        
            return true;
        
    
    

正如文章中提到的,这个扩展并不完全通用,但如果你能得到一个列表,它就可以工作。

【讨论】:

绝对完美。谢谢。 我很困惑 - 我的 WriteJson 抛出未实现?我需要处理这个吗? @nologo 仅当您计划调用 WriteJson 时。这个问题是关于阅读 JSON 的【参考方案2】:

JSON.NET 的开发人员最终帮助了项目 codeplex 网站。这是解决方案:

问题是,当它是一个 JSON 对象时,我没有读取过去的属性。这是正确的代码:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

    if (reader.TokenType == JsonToken.StartArray)
    
        return serializer.Deserialize<List<FacebookMedia>>(reader);
    
    else
    
        FacebookMedia media = serializer.Deserialize<FacebookMedia>(reader);
        return new List<FacebookMedia>(new[] media);
    

James 也很友好地为上述方法提供了单元测试。

【讨论】:

【参考方案3】:

根据上面 Camilo Martinez 的回答,这是一种更现代、类型安全、更精简和更完整的方法,它使用 JsonConverter 和 C# 8.0 的通用版本以及实现序列化部分。除了根据问题预期的两个令牌之外,它还会引发异常。代码不应超出要求,否则您可能会因错误处理意外数据而导致未来出现错误。

internal class SingleObjectOrArrayJsonConverter<T> : JsonConverter<ICollection<T>> where T : class, new()

    public override void WriteJson(JsonWriter writer, ICollection<T> value, JsonSerializer serializer)
    
        serializer.Serialize(writer, value.Count == 1 ? (object)value.Single() : value);
    

    public override ICollection<T> ReadJson(JsonReader reader, Type objectType, ICollection<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
    
        return reader.TokenType switch
        
            JsonToken.StartObject => new Collection<T> serializer.Deserialize<T>(reader),
            JsonToken.StartArray => serializer.Deserialize<ICollection<T>>(reader),
            _ => throw new ArgumentOutOfRangeException($"Converter does not support JSON token type reader.TokenType.")
        ;
    

然后这样装饰属性:

[JsonConverter(typeof(SingleObjectOrArrayJsonConverter<OrderItem>))]
public ICollection<OrderItem> items;

我已将属性类型从List&lt;&gt; 更改为ICollection&lt;&gt;,因为JSON POCO 通常只需要这种较弱的类型,但如果需要List&lt;&gt;,则只需将ICollectionCollection 替换为@ 987654329@ 以上所有代码。

【讨论】:

我想你有错字? EmptyObjectOrArrayJsonConverter 应该是 SingleObjectOrArrayJsonConverter ?【参考方案4】:

看看 c# 框架中的 System.Runtime.Serialization 命名空间,它会让你很快到达你想去的地方。

如果你愿意,你可以查看this 项目中的一些示例代码(不是试图插入我自己的工作,但我刚刚完成了你正在做的事情,只是使用了不同的源 api。

希望对你有帮助。

【讨论】:

-1 推荐使用框架的内部经过验证的部分而不是带有代码示例链接的第三方库? 我不确定 -1 是从哪里来的。我很欣赏你的回应。我怀疑这可能是因为它是一个小问题的相当重量级的解决方案(很可能是由于我自己滥用 JSON.NET)。话虽如此,我将尝试使用 Systme.Runtime.Serialization 方法,看看它是否最终效果更好。感谢您的链接。 没问题,我只是建议它,因为我在大约一个小时内从一无所有(无法序列化/反序列化)到工作功能代码......认为这将是一个合理的时间你来看看它是否适合你! 并提示,只需在我发送给你的 Client/Extensions.cs 链接中获取 2 个扩展方法 :) 原来我的代码中有一个错误。我确实尝试过使用 System.Runtime.Serialization 方法,它们似乎在我有限的测试中有效。事实证明,错误修复比转换所有内容要容易得多。尽管为帮助而投票。再次感谢。【参考方案5】:

.Net 框架

using Newtonsoft.Json;
using System.IO;   

public Object SingleObjectOrArrayJson(string strJson)
   
    if(String.IsNullOrEmpty(strJson))
    
       //Example
       strJson= @"
        'CPU': 'Intel',
        'PSU': '500W',
        'Drives': [
          'DVD read/writer'
          /*(broken)*/,
          '500 gigabyte hard drive',
          '200 gigabyte hard drive'
        ]
      ";
    

    JsonTextReader reader = new JsonTextReader(new StringReader(strJson));
    
    //Initialize Read
    reader.Read();
    
        if (reader.TokenType == JsonToken.StartArray)
        
            return JsonConvert.DeserializeObject<List<Object>>(strJson);
        
        else
        
            Object media = JsonConvert.DeserializeObject<Object>(strJson);
            return new List<Object>(new[] media);
        

注意: “对象”必须根据响应的Json属性来定义

【讨论】:

【参考方案6】:

阐述 Martinez 和 mfanto 对 Newtonsoft 的回答。它确实适用于 Newtonsoft:

这是一个使用数组而不是列表(并正确命名)的示例。

public class SingleValueArrayConverter<T> : JsonConverter

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        if (reader.TokenType == JsonToken.StartObject 
            || reader.TokenType == JsonToken.String
            || reader.TokenType == JsonToken.Integer)
        
            return new T[]  serializer.Deserialize<T>(reader) ;
        
        return serializer.Deserialize<T[]>(reader);
    

    public override bool CanConvert(Type objectType)
    
        return true;
    

然后在属性上面写这个:

[JsonProperty("INSURANCE")]
[JsonConverter(typeof(SingleValueArrayConverter<InsuranceInfo>))]
public InsuranceInfo[] InsuranceInfo  get; set; 

Newtonsoft 将为您完成剩下的工作。

return JsonConvert.DeserializeObject<T>(json);

为 Martinez 和 mfanto 干杯!

信不信由你,这将适用于子项目。 (甚至可能必须这样做。)所以...在我的 InsuranceInfo 中,如果我有另一个对象/数组混合体,请在该属性上再次使用它。

这也将允许您将对象重新序列化回 json。当它重新序列化时,它将始终是一个数组。

【讨论】:

【参考方案7】:

我认为你应该这样写你的课......!!!

public class FacebookAttachment
    

        [JsonProperty("attachment")]
        public Attachment Attachment  get; set; 

        [JsonProperty("permalink")]
        public string Permalink  get; set; 
    

public class Attachment
    

        [JsonProperty("media")]
        public Media Media  get; set; 

        [JsonProperty("name")]
        public string Name  get; set; 

        [JsonProperty("caption")]
        public string Caption  get; set; 

        [JsonProperty("description")]
        public string Description  get; set; 

        [JsonProperty("properties")]
        public Properties Properties  get; set; 

        [JsonProperty("icon")]
        public string Icon  get; set; 

        [JsonProperty("fb_object_type")]
        public string FbObjectType  get; set; 
    
 public class Media
    
    
 public class Properties
    
    

【讨论】:

以上是关于有时是数组有时是对象时反序列化JSON的主要内容,如果未能解决你的问题,请参考以下文章

如何处理 API 有时将属性作为数组返回,有时作为对象返回?

如何使用json.net自定义反序列化为对象

简单Json序列化和反序列化

使用Jackson在android中反序列化具有相同键但不同类型的json

使用 Newtonsoft Json 从流中反序列化多个 json 对象

从Kafka主题消费消息时反序列化的问题