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

61

我使用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,会出现错误:Cannot deserialize JSON object into type '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的值是"permalink"。明显可以看到,在switch语句中没有为JsonToken.PropertyName设置case。

我的转换器需要做什么不同的事情吗?感谢任何帮助。


这个回答解决了你的问题吗?如何使用JSON.net处理同一属性的单个项目和数组 - Self
7个回答

53

关于如何处理这种情况的非常详细的解释,请查看"使用自定义JsonConverter修复错误的JSON结果"

总之,您可以扩展默认的JSON.NET转换器,方法如下:

  1. Annotate the property with the issue

    [JsonConverter(typeof(SingleValueArrayConverter<OrderItem>))]
    public List<OrderItem> items;
    
  2. Extend the converter to return a list of your desired type even for a single object

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

正如文章中所提到的,此扩展程序并不完全通用,但如果您只需要列表,则它可以发挥作用。


2
非常完美。谢谢。 - NotMe
我很困惑 - 我的 WriteJson 抛出了未实现的异常?我需要处理吗? - nologo
1
只有在您计划调用WriteJson时才需要使用@nologo。这个问题是关于读取JSON的。 - Caius Jard

26

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 也很友善地为上述方法提供了单元测试。


5

基于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<>更改为ICollection<>,因为JSON POCO通常只需要这种较弱的类型,但如果需要List<>,则只需在上面的所有代码中用List替换ICollectionCollection


1
我认为你打错字了吗?EmptyObjectOrArrayJsonConverter应该是SingleObjectOrArrayJsonConverter? - shelbypereira

4

针对Newtonsoft,进一步阐述Martinez和mfanto的答案。它确实可以与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。当它重新序列化时,它将始终是一个数组。


2

请看一下C#框架中的System.Runtime.Serialization命名空间,它可以让你快速到达目标。

如果您愿意,您可以在这个项目中查看一些示例代码(我不是在推销自己的作品,但我刚刚完成了与不同来源API几乎完全相同的工作)。

希望能帮到您。


1
推荐使用框架内部经过验证的部分,而不是使用第三方库,附带代码示例,评分为-1。 - jonezy
我不确定为什么会出现-1。感谢您的回复。我怀疑这可能是因为它是一个相对较重的解决方案,用于解决一个较小的问题(很可能是由于我自己对JSON.NET的误用)。话虽如此,我将尝试使用System.Runtime.Serialization方法,并查看是否能够更好地解决问题。感谢提供链接。 - mfanto
没问题,我只是建议这样做,因为我从零开始(没有序列化/反序列化的能力)到一个工作的功能代码只用了大约一个小时...我认为这对你来说是一个合理的时间来确定它是否适合你! - jonezy
只需获取我发送的Client/Extensions.cs链接中的2个扩展方法即可 :) - jonezy
原来我的代码有一个漏洞。我尝试使用System.Runtime.Serialization方法进行测试,似乎在有限的测试中它们是有效的。但修复这个漏洞比转换所有内容要容易得多。还是感谢你的帮助,我会点赞的。谢谢! - mfanto

1

.Net Framework

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

注意: "Object" 必须根据您的响应的 Json 属性进行定义。

-3

我认为你应该像这样编写你的类...!!!

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

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