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;
public class SingleOrArrayListItemConverter<TItem> : JsonConverter
readonly JsonConverter itemConverter;
public SingleOrArrayListItemConverter(Type itemConverterType) : this(itemConverterType, true) { }
public SingleOrArrayListItemConverter(Type itemConverterType, bool canWrite)
this.itemConverter = (JsonConverter)Activator.CreateInstance(itemConverterType);
this.canWrite = canWrite;
public override bool CanConvert(Type objectType)
return typeof(List<TItem>).IsAssignableFrom(objectType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
if (reader.MoveToContent().TokenType == JsonToken.Null)
var contract = serializer.ContractResolver.ResolveContract(objectType);
var list = (ICollection<TItem>)(existingValue ?? contract.DefaultCreator());
if (reader.TokenType != JsonToken.StartArray)
list.Add(ReadItem(reader, serializer));
while (reader.ReadToContent())
switch (reader.TokenType)
list.Add(ReadItem(reader, serializer));
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
TItem ReadItem(JsonReader reader, JsonSerializer serializer)
if (itemConverter.CanRead)
return (TItem)itemConverter.ReadJson(reader, typeof(TItem), default(TItem), serializer);
return serializer.Deserialize<TItem>(reader);
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
var list = value as ICollection<TItem>;
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
foreach (var item in list)
WriteItem(writer, item, serializer);
writer.WriteStartArray();
foreach (var item in list)
WriteItem(writer, item, serializer);
void WriteItem(JsonWriter writer, TItem value, JsonSerializer serializer)
if (itemConverter.CanWrite)
itemConverter.WriteJson(writer, value, serializer);
serializer.Serialize(writer, value);
public class ConcreteConverter<IInterface, TConcrete> : JsonConverter where TConcrete : IInterface
public override bool CanConvert(Type objectType)
return typeof(IInterface) == objectType;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
return serializer.Deserialize<TConcrete>(reader);
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
throw new NotImplementedException();
public static partial class JsonExtensions
public static JsonReader MoveToContent(this JsonReader reader)
if (reader.TokenType == JsonToken.None)
while (reader.TokenType == JsonToken.Comment && reader.Read())
public static bool ReadToContent(this JsonReader reader)
while (reader.TokenType == JsonToken.Comment)
public interface IBatchList
List<IBatchItems> Items { get; set; }
public interface IBatchItems
JObject Info { get; set; }
[JsonObject(MemberSerialization.OptIn)]
public class BatchList : IBatchList
[JsonProperty(PropertyName = "Items", Required = Required.Always)]
[JsonConverter(typeof(SingleOrArrayListItemConverter<IBatchItems>), typeof(ConcreteConverter<IBatchItems, BatchItems>))]
public List<IBatchItems> Items { get; set; }
public class BatchItems : IBatchItems
[JsonProperty(PropertyName = "Id", Required = Required.Always)]
public string Id { get; set; }
[JsonProperty(PropertyName = "Info", Required = Required.Always)]
public JObject Info { get; set; }
public static void Test()
DeserializeAndPost(@"{'Items': [{'Id': 'name1','Info': {'age': '20'}},{'Id': 'name2','Info': {'age': '21'}}]}");
DeserializeAndPost(@"{'Items': {'Id': 'name1','Info': {'age': '20'}}}");
DeserializeAndPost(@"{'Items': []}");
DeserializeAndPost(@"{'Items': [{'Id': 'name1','Info': {'age': '20'}},null,{'Id': 'name2','Info': {'age': '21'}}]}");
Console.WriteLine("\nAll tests passed.");
public static void DeserializeAndPost(string json)
IBatchList req = JsonConvert.DeserializeObject<BatchList>(json);
public static void Post(IBatchList batchList, string oldJson)
var newJson = JsonConvert.SerializeObject(batchList);
Console.WriteLine(newJson);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(newJson), JToken.Parse(oldJson)));
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: ");