0

I need to consume JSON from a REST service. For one of the fields, the service sometimes returns a string for the value yet other times returns an array of strings for the value.

For example:

var jsonArrayResponse = "{ \"id\" : \"abc\", \"relatedIds\" : [ \"def\", \"ghi\", \"jkl\" ] }";
var jsonSingleResponse = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";

I have implemented the destination class using the SingleValueArrayConverter pattern found on this post. Here is the full set of code to illustrate my problem:

class Program
{
    static void Main(string[] args)
    {
        // this works fine with the SingleValueArrayConverter
        var jsonArray = "{ \"id\" : \"abc\", \"relatedIds\" : [ \"def\", \"ghi\", \"jkl\" ] }";
        var objArray = (MyObject)JsonConvert.DeserializeObject(jsonArray, typeof(MyObject));

        // this fails to parse "456" with Exception message:
        //      "Unable to cast object of type 'System.Object' to 
        //       type 'System.Collections.Generic.List`1[System.String]'."
        var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";
        var objSingle = (MyObject)JsonConvert.DeserializeObject(jsonSingle, typeof (MyObject));
    }
}

[DataContract]
public class MyObject
{
    [DataMember(Name = "id")]
    public string Id;

    [DataMember(Name = "relatedIds")]
    [JsonConverter(typeof(SingleValueArrayConverter<string>))]
    public List<string> RelatedIds;
}

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

How can I fix this to properly parse the two different JSON blobs into the same class?

UPDATE:

I modified the SingleValueArrayConverter to check the string case (as mentioned below), and things started working. I use the converter for more than just strings, so I had to add the first if statement, rather than modify it as suggested).

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object retVal = new Object();

        if (reader.TokenType == JsonToken.String)
        {
            string instance = (string)serializer.Deserialize(reader);
            retVal = new List<string> () {instance};
        }

        else 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 false;
    }
}
Community
  • 1
  • 1
RobK
  • 13
  • 1
  • 6

2 Answers2

1

You could change this:

var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";

To this:

var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : [ \"456\" ] }";

...and accept that relatedIds has one or more items therefore should always be a collection.

Alternatively you might make RelatedIds a generic:

public class MyObject<T>
{
    // .... //

    public T RelatedIds;
}

Which you could use like this:

var objArray = (MyObject<List<string>>)JsonConvert.DeserializeObject(jsonArray, typeof(MyObject<List<string>>));

var objSingle = (MyObject<object>)JsonConvert.DeserializeObject(jsonSingle, typeof (MyObject<object>));

Personally I prefer the previous option, for simplicity.

One thing you cannot do is make something defined as a string suddenly become a List<string> at runtime (actually maybe you could using dynamic, but better not to go there...). That is not allowed in a statically-typed language (though is possible in weakly typed languages such as JS or PHP).

garryp
  • 5,508
  • 1
  • 29
  • 41
  • Thanks for the quick reply! Unfortunately I don't have control of the structure of the JSON -- it would be hard to find the single string to wrap it in array braces, and the service doesn't indicate which "flavor" of response I'm getting so I won't be able to use your second option. – RobK May 21 '15 at 18:09
0

Change JsonToken.StartObject to JsonToken.String. If you put a breakpoint right on the line object retVal = new Object(); you can see that reader.TokenType is JsonToken.String during the second instantiation. That's the only problem with your code.

Dax Fohl
  • 10,654
  • 6
  • 46
  • 90