0

Hopefully someone can help me with my issue.

I'm working with a 3rd party API that, depending on the configuration of the product in the backend, will return a different structure of JSON as the response.

I've included JSON samples showing the different outcomes depending on the product. There are 3 scenarios for the FreeGifts section of the product:

  1. Has 2 or more free gifts.
  2. Has 1 free gifts.
  3. Has no free gifts

Scenario 1

{
    "FreeGifts": [{
        "FreeGift": [{
                "SKU": "BOWS-SMALL-ALFIE"
            },
            {
                "SKU": "BOWS-LARGE-ALONZO"
            },
            {
                "SKU": "BOWS-LARGE-CLANCY"
            },
            {
                "SKU": "BOWS-SMALL-ALVIN"
            },
            {
                "SKU": "BOWS-SMALL-CLARK"
            }
        ]
    }]
}

Scenario 2

{
    "FreeGifts": [{
        "FreeGift": {
            "SKU": "BOWS-SMALL-ALVIN"
        }
    }]
}

Scenario 3

{
    "FreeGifts": [
        ""
    ]
}

Sites like http://json2csharp.com/ and https://jsonutils.com/ provide me with 3 different class definitions depending on the scenario.

If I had only 1 of these in the structure I could probably deal with it but I have around 7 or 8. It's impossible for me to cater for it.

I'm completely stumped as to how I get Newtonsoft.json to work with the ambiguity that the API produces.

Do I need to go back to the provider and ask them if they can change it?!?

dbc
  • 104,963
  • 20
  • 228
  • 340
Skin
  • 9,085
  • 2
  • 13
  • 29
  • Can you please provide your json in text format here? – Mihir Dave Mar 05 '18 at 11:25
  • Possibly you want [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/q/18994685/3744182). – dbc Mar 05 '18 at 23:40
  • Yeah yeah, I know. I put the image in because it was easier to show the comparison between the JSON and the generated classes. Edits have been made to show the JSON. – Skin Mar 06 '18 at 00:56

1 Answers1

0

You can use SingleOrArrayConverter<FreeGift> from this answer to How to handle both a single item and an array for the same property using JSON.net by Brian Rogers along with TolerantObjectCollectionConverter<FreeGifts> from this answer to How can I ignore a blank array inside an array of JSON objects while deserializing? to successfully deserialize all 3 JSON variants. To do so, define your model and apply the appropriate JSON converter as follows:

public class Root
{
    [JsonConverter(typeof(TolerantObjectCollectionConverter<FreeGifts>))]
    public List<FreeGifts> FreeGifts { get; set; }
}

public class FreeGifts
{
    [JsonConverter(typeof(SingleOrArrayConverter<FreeGift>))]
    public List<FreeGift> FreeGift { get; set; }
}

public class FreeGift
{
    public string SKU { get; set; }
}

class SingleOrArrayConverter<T> : JsonConverter
{
    // Taken from the answer to 
    // https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
    // https://stackoverflow.com/a/18997172
    // by Brian Rogers
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var tokenType = reader.SkipComments().TokenType;
        if (tokenType == JsonToken.Null)
            return null;
        var list = existingValue as List<T> ?? new List<T>();
        if (tokenType == JsonToken.StartArray)
        {
            serializer.Populate(reader, list);
        }
        else
        {
            list.Add(serializer.Deserialize<T>(reader));
        }
        return list;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public class TolerantObjectCollectionConverter<TItem> : JsonConverter
{
    // Taken from the answer to 
    // https://stackoverflow.com/questions/49030516/how-can-i-ignore-a-blank-array-inside-an-array-of-json-objects-while-deserializi
    // https://stackoverflow.com/a/49078620/

    public override bool CanConvert(Type objectType)
    {
        return !objectType.IsArray && objectType != typeof(string) && typeof(ICollection<TItem>).IsAssignableFrom(objectType);
    }

    public override bool CanWrite { get { return false; } }

    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)
    {
        // Get contract information
        var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonArrayContract;
        if (contract == null || contract.IsMultidimensionalArray || objectType.IsArray)
            throw new JsonSerializationException(string.Format("Invalid array contract for {0}", objectType));

        // Process the first token
        var tokenType = reader.SkipComments().TokenType;
        if (tokenType == JsonToken.Null)
            return null;
        if (tokenType != JsonToken.StartArray)
            throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));

        // Allocate the collection
        var collection = existingValue as ICollection<TItem> ?? (ICollection<TItem>)contract.DefaultCreator();

        // Process the collection items
        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.EndArray:
                    return collection;

                case JsonToken.StartObject:
                case JsonToken.Null:
                    collection.Add(serializer.Deserialize<TItem>(reader));
                    break;

                default:
                    reader.Skip();
                    break;
            }
        }
        // Should not come here.
        throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
    }
}

public static partial class JsonExtensions
{
    public static JsonReader SkipComments(this JsonReader reader)
    {
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            ;
        return reader;
    }
}

Notes:

  • [JsonConverter(typeof(TolerantObjectCollectionConverter<FreeGifts>))] handles the fact that the upper-level "FreeGifts": [] array may sometimes contain an unwanted string value. The value is simply skipped.

  • [JsonConverter(typeof(SingleOrArrayConverter<FreeGift>))] handles the fact that the "FreeGift" property value may sometimes be either a single object or an array of objects.

  • You could combine the two converters in situations where a property value might be an array with invalid items or a single object not contained in an array. However, this isn't the case in the three JSON examples shown.

Sample working .Net fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks man, will check this out. I got the full JSON working last night so now just need to break it back and reduce the JSON to provide all 3 scenarios so I can work through the converters. I'll mark it as the answer given it goes into enough detail about the custom converters. Very clever piece of kit. – Skin Mar 07 '18 at 02:47