60

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.

Community
  • 1
  • 1
mfanto
  • 14,168
  • 6
  • 51
  • 61
  • Does this answer your question? [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n) – Self Mar 05 '21 at 09:54

7 Answers7

53

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.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
Camilo Martinez
  • 1,723
  • 2
  • 21
  • 26
26

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.

svick
  • 236,525
  • 50
  • 385
  • 514
mfanto
  • 14,168
  • 6
  • 51
  • 61
5

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.

Neo
  • 4,145
  • 6
  • 53
  • 76
4

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.

Patrick Knott
  • 1,666
  • 15
  • 15
2

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.

jonezy
  • 1,903
  • 13
  • 21
  • 1
    -1 for recommneding use of an internal proven part of the framework over a 3rd party library with links to code samples? – jonezy Mar 07 '11 at 21:06
  • 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. – mfanto Mar 07 '11 at 21:37
  • 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 Mar 07 '11 at 21:39
  • and hint, just grab the 2 extension methods in the Client/Extensions.cs link i sent ya :) – jonezy Mar 07 '11 at 21:39
  • 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. – mfanto Mar 08 '11 at 00:20
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});
        }
}

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

-3

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
    {
    }
CMW
  • 3,093
  • 1
  • 16
  • 20
radhey_mishra
  • 137
  • 3
  • 18