using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
[JsonConverter(typeof(TolerantObjectCollectionConverter<FreeGifts>))]
public List<FreeGifts> FreeGifts { get; set; }
[JsonConverter(typeof(SingleOrArrayConverter<FreeGift>))]
public List<FreeGift> FreeGift { get; set; }
public string SKU { get; set; }
class SingleOrArrayConverter<T> : JsonConverter
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)
var list = existingValue as List<T> ?? new List<T>();
if (tokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
list.Add(serializer.Deserialize<T>(reader));
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
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)
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));
var tokenType = reader.SkipComments().TokenType;
if (tokenType == JsonToken.Null)
if (tokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));
var collection = existingValue as ICollection<TItem> ?? (ICollection<TItem>)contract.DefaultCreator();
switch (reader.TokenType)
case JsonToken.StartObject:
collection.Add(serializer.Deserialize<TItem>(reader));
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())
public static void Test()
foreach (var json in GetJson())
Console.WriteLine("\nDone.");
static void Test(string json)
var root = JsonConvert.DeserializeObject<Root>(json);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented);
var count1 = JToken.Parse(json).SelectTokens("..SKU").Count();
var count2 = (root.FreeGifts ?? Enumerable.Empty<FreeGifts>()).SelectMany(f => f.FreeGift ?? Enumerable.Empty<FreeGift>()).Count();
Assert.IsTrue(count1 == count2);
Console.WriteLine("\nRe-serialized JSON:");
Console.WriteLine(json2);
static IEnumerable<string> GetJson()
""SKU"": ""BOWS-SMALL-ALFIE""
""SKU"": ""BOWS-LARGE-ALONZO""
""SKU"": ""BOWS-LARGE-CLANCY""
""SKU"": ""BOWS-SMALL-ALVIN""
""SKU"": ""BOWS-SMALL-CLARK""
""SKU"": ""BOWS-SMALL-ALVIN""
public static void Main()
Console.WriteLine("Environment version: " + Environment.Version);
Console.WriteLine("Json.NET version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");
public class AssertionFailedException : System.Exception
public AssertionFailedException() : base() { }
public AssertionFailedException(string s) : base(s) { }
public static class Assert
public static void IsTrue(bool value)
public static void IsTrue(bool value, string message)
throw new AssertionFailedException(message ?? "failed");