using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Collections.ObjectModel;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
public class SingleOrArrayListConverter : JsonConverter
readonly IContractResolver resolver;
public SingleOrArrayListConverter() : this(false) { }
public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }
public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
this.canWrite = canWrite;
this.resolver = resolver ?? new JsonSerializer().ContractResolver;
static bool CanConvert(Type objectType, IContractResolver resolver)
JsonArrayContract contract;
return CanConvert(objectType, resolver, out itemType, out contract);
static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
if ((itemType = objectType.GetListItemType()) == null)
if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
var itemContract = resolver.ResolveContract(itemType);
if (itemContract is JsonArrayContract)
public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
JsonArrayContract contract;
if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
if (reader.MoveToContent().TokenType == JsonToken.Null)
var list = (IList)(existingValue ?? contract.DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
list.Add(serializer.Deserialize(reader, itemType));
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
var list = value as ICollection;
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
public static partial class JsonExtensions
public static JsonReader MoveToContent(this JsonReader reader)
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
internal static Type GetListItemType(this Type type)
if (type.IsPrimitive || type.IsArray || type == typeof(string))
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
where TCollection : ICollection<TItem>
public SingleOrArrayCollectionConverter() : this(false) { }
public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }
public override bool CanConvert(Type objectType)
return typeof(TCollection).IsAssignableFrom(objectType);
static void ValidateItemContract(IContractResolver resolver)
var itemContract = resolver.ResolveContract(typeof(TItem));
if (itemContract is JsonArrayContract)
throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
ValidateItemContract(serializer.ContractResolver);
if (reader.MoveToContent().TokenType == JsonToken.Null)
var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
list.Add(serializer.Deserialize<TItem>(reader));
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
ValidateItemContract(serializer.ContractResolver);
var list = value as ICollection<TItem>;
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
public static void Test()
TestSingleOrArrayListConverter.Test();
TestSingleOrArrayCollectionConverter.Test();
Console.WriteLine("\nAll tests passed.");
class TestSingleOrArrayCollectionConverter
public static void Test()
TestObservableCollectionNoWriteRoundTrip();
TestObservableCollectionRoundTrip();
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
static void TestQuestion()
""email"": ""john.doe@sendgrid.com"",
""timestamp"": 1337966815,
""email"": ""jane.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
""email"": ""john.doe@sendgrid.com"",
""timestamp"": 1337966815,
""email"": ""jane.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": [""olduser""],
var settings = new JsonSerializerSettings
ContractResolver = new CamelCasePropertyNamesContractResolver(),
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);
var json2 = JsonConvert.SerializeObject(list, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(expectedJson), JToken.Parse(json2)));
static void TestObservableCollectionNoWriteRoundTrip()
var expectedJsonNoWrite = @"[
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayCollectionConverter<ObservableCollection<string>, string>() },
var root = JsonConvert.DeserializeObject<List<ObservableCollection<string>>>(json, settings);
Assert.IsTrue(root.Count == 5 && root[0] == null && root[1].Count == 0 && root[2].Count == 1 && root[3].Count == 2 && root[4].Count == 3);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(expectedJsonNoWrite), JToken.Parse(json2)));
static void TestObservableCollectionRoundTrip()
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayCollectionConverter<ObservableCollection<string>, string>(true) },
var root = JsonConvert.DeserializeObject<List<ObservableCollection<string>>>(json, settings);
Assert.IsTrue(root.Count == 5 && root[0] == null && root[1].Count == 0 && root[2].Count == 1 && root[3].Count == 2 && root[4].Count == 3);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(json2)));
class TestSingleOrArrayListConverter
public static void Test()
TestNotImplementedCases();
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
public List<string> Category { get; set; }
static void TestQuestion()
""email"": ""john.doe@sendgrid.com"",
""timestamp"": 1337966815,
""email"": ""jane.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayListConverter(true) },
ContractResolver = new CamelCasePropertyNamesContractResolver(),
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);
var json2 = JsonConvert.SerializeObject(list, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(json2)));
static void TestNotImplementedCases()
var converter = new SingleOrArrayListConverter();
Assert.IsTrue(converter.CanConvert(typeof(ListObject<string>)) == false);
Assert.IsTrue(converter.CanConvert(typeof(string)) == false);
Assert.IsTrue(converter.CanConvert(typeof(List<int[]>)) == false);
Assert.IsTrue(converter.CanConvert(typeof(List<ObservableCollection<List<string>>>)) == false);
Assert.IsTrue(converter.CanConvert(typeof(JToken)) == false);
Assert.IsTrue(converter.CanConvert(typeof(object)) == false);
static void TestListObject()
var collection = new ListObject<string> { "one" };
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayListConverter(true) },
var token1 = JToken.FromObject(collection, JsonSerializer.Create(settings));
var token2 = JToken.FromObject(collection, JsonSerializer.Create(null));
Assert.IsTrue(JToken.DeepEquals(token1, token2));
var json = token2.ToString();
var collection2 = JsonConvert.DeserializeAnonymousType(json, collection, settings);
Assert.IsTrue(collection.SequenceEqual(collection2));
static void TestWriteRoundTrip()
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayListConverter(true) },
var root = JsonConvert.DeserializeObject<List<List<string>>>(json, settings);
Assert.IsTrue(root.Count == 5 && root[0] == null && root[1].Count == 0 && root[2].Count == 1 && root[3].Count == 2 && root[4].Count == 3);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(json2)));
static void TestNoWriteRoundTrip()
var expectedJsonNoWrite = @"[
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayListConverter() },
var root = JsonConvert.DeserializeObject<List<List<string>>>(json, settings);
Assert.IsTrue(root.Count == 5 && root[0] == null && root[1].Count == 0 && root[2].Count == 1 && root[3].Count == 2 && root[4].Count == 3);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(expectedJsonNoWrite), JToken.Parse(json2)));
static void TestCommentRoundTrip()
var expectedJsonNoWrite = @"[
[""double"", ""double""],
[""triple"", ""triple"", ""triple""]
/* Comment */[""double"", ""double""],
/* Comment */[""triple"", ""triple"", ""triple""]
var settings = new JsonSerializerSettings
Converters = { new SingleOrArrayListConverter() },
var root = JsonConvert.DeserializeObject<List<List<string>>>(json, settings);
Assert.IsTrue(root.Count == 5 && root[0] == null && root[1].Count == 0 && root[2].Count == 1 && root[3].Count == 2 && root[4].Count == 3);
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(expectedJsonNoWrite), JToken.Parse(json2)));
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
class ListObject<T> : List<T>
int MyCount { get { return Count; } }
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: ");