Deserializing JSON when sometimes array and sometimes object

C#Jsonjson.netFacebook C#-SdkJson Deserialization

C# Problem Overview


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: https://stackoverflow.com/questions/4572732/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.

C# Solutions


Solution 1 - C#

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 2 - C#

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 3 - C#

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 - C#

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 - C#

.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

Solution 6 - C#

Expounding upon Martinez and mfanto's answer for Newtonsoft. It does work with Newtonsoft:

Here is an example of doing it with an array instead of a list (and correctly named).

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

Then over the attribute write this:

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

Newtonsoft will do the rest for you.

return JsonConvert.DeserializeObject<T>(json);

Cheers to Martinez and mfanto!

Believe it or not, this will work with sub items. (It may even have to.) So... inside of my InsuranceInfo, if I have another object/array hybrid, use this again on that property.

This will also allow you to reserialize the object back to json. When it does reserialize, it will always be an array.

Solution 7 - C#

I think you should write your class like this...!!!

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmfantoView Question on Stackoverflow
Solution 1 - C#Camilo MartinezView Answer on Stackoverflow
Solution 2 - C#mfantoView Answer on Stackoverflow
Solution 3 - C#NeoView Answer on Stackoverflow
Solution 4 - C#jonezyView Answer on Stackoverflow
Solution 5 - C#Cesar BetancurthView Answer on Stackoverflow
Solution 6 - C#Patrick KnottView Answer on Stackoverflow
Solution 7 - C#radhey_mishraView Answer on Stackoverflow