Deserializing JSON when sometimes array and sometimes object

32,281

Solution 1

The developer of JSON.NET ended up helping on the projects codeplex site. Here is the solution:

The problem was, when it was a JSON object, I wasn't reading past the attribute. Here is the correct code:

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 was also kind enough to provide unit tests for the above method.

Solution 2

A very detailed explanation on how to handle this case is available at "Using a Custom JsonConverter to fix bad JSON results".

To summarize, you can extend the default JSON.NET converter doing

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

As mentioned in the article this extension is not completely general but it works if you are fine with getting a list.

Solution 3

Based on Camilo Martinez's answer above, this is a more modern, type-safe, leaner and complete approach using the generic version of JsonConverter and C# 8.0 as well as implementing the serialization part. It also throws an exception for tokens other than the two expected according to the question. Code should never do more than required otherwise you run the risk of causing a future bug due to mishandling unexpected data.

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

And then decorate the property thus:

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

I've changed the property type from List<> to ICollection<> as a JSON POCO typically need only be this weaker type, but if List<> is required, then just replaced ICollection and Collection with List in all the above code.

Solution 4

take a look at the System.Runtime.Serialization namespace in the c# framework, it's going to get you to where you want to be very quickly.

If you want, you can check out some example code in this project (not trying to plug my own work but i just finished pretty much exactly what you are doing but with a different source api.

hope it helps.

Solution 5

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

Note: "Object" must be defined according to the Json attributes of your response

Share:
32,281
mfanto
Author by

mfanto

Updated on July 09, 2022

Comments

  • mfanto
    mfanto almost 2 years

    I'm having a bit of trouble deserializing data returned from Facebook using the JSON.NET libraries.

    The JSON returned from just a simple wall post looks like:

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

    The JSON returned for a photo looks like:

    "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"}}
            ],
    

    Everything works great and I have no problems. I've now come across a simple wall post from a mobile client with the following JSON, and deserialization now fails with this one single post:

    "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"
    

    Here is the class I am deserializing as:

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

    Without using the FacebookMediaJsonConverter, I get an error: Cannot deserialize JSON object into type 'System.Collections.Generic.List`1[FacebookMedia]'. which makes sense, since in the JSON, Media is not a collection.

    I found this post which describes a similar problem, so I've attempted to go down this route: Deserialize JSON, sometimes value is an array, sometimes "" (blank string)

    My converter looks like:

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

    Which works fine, except I now get a new exception:

    Inside JsonSerializerInternalReader.cs, CreateValueInternal(): Unexpected token while deserializing object: PropertyName

    The value of reader.Value is "permalink". I can clearly see in the switch that there's no case for JsonToken.PropertyName.

    Is there something I need to do differently in my converter? Thanks for any help.

  • jonezy
    jonezy about 13 years
    -1 for recommneding use of an internal proven part of the framework over a 3rd party library with links to code samples?
  • mfanto
    mfanto about 13 years
    I'm not sure where the -1 came from. I appreciate the response. I suspect it's probably from it being a fairly heavyweight solution to a minor problem (most likely due to my own misuse of JSON.NET). That being said, I'm going to give the Systme.Runtime.Serialization methods a try and see if it ends up working out better. Thanks for the link.
  • jonezy
    jonezy about 13 years
    no problem, I only suggested it because I got literally from nothing (no ability to serialize/deserialize) to working functional code in about an hour... thought that would be a reasonable amount of time for you to figure out if it works for ya !
  • jonezy
    jonezy about 13 years
    and hint, just grab the 2 extension methods in the Client/Extensions.cs link i sent ya :)
  • mfanto
    mfanto about 13 years
    It turns out there was a bug in my code. I did play around with using the System.Runtime.Serialization methods, and they seemed to work during my limited testing. The bug fix turned out to be a lot easier than converting everything over though. Upvoted though for the help. Thanks again.
  • NotMe
    NotMe about 9 years
    Absolutely perfect. Thank you.
  • nologo
    nologo about 7 years
    i'm confused - my WriteJson is throwing not implemented? do i need to handle this?
  • Caius Jard
    Caius Jard almost 5 years
    @nologo only if you plan on calling WriteJson. This question is about reading JSON
  • shelbypereira
    shelbypereira over 4 years
    I think you have a typo? EmptyObjectOrArrayJsonConverter should be SingleObjectOrArrayJsonConverter ?