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?!?

3
  • Can you please provide your json in text format here? Commented Mar 5, 2018 at 11:25
  • Possibly you want How to handle both a single item and an array for the same property using JSON.net. Commented Mar 5, 2018 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. Commented Mar 6, 2018 at 0:56

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.