using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
public class SingleOrArrayConverter<TItem> : SingleOrArrayConverter<List<TItem>, TItem>
public SingleOrArrayConverter() : this(true) { }
public SingleOrArrayConverter(bool canWrite) : base(canWrite) { }
public class SingleOrArrayConverterFactory : JsonConverterFactory
public bool CanWrite { get; }
public SingleOrArrayConverterFactory() : this(true) { }
public SingleOrArrayConverterFactory(bool canWrite) => CanWrite = canWrite;
public override bool CanConvert(Type typeToConvert)
var itemType = GetItemType(typeToConvert);
if (itemType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(itemType))
if (typeToConvert.GetConstructor(Type.EmptyTypes) == null || typeToConvert.IsValueType)
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
var itemType = GetItemType(typeToConvert);
var converterType = typeof(SingleOrArrayConverter<,>).MakeGenericType(typeToConvert, itemType);
return (JsonConverter)Activator.CreateInstance(converterType, new object [] { CanWrite });
static Type GetItemType(Type type)
if (type.IsPrimitive || type.IsArray || type == typeof(string))
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
public class SingleOrArrayConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
public SingleOrArrayConverter() : this(true) { }
public SingleOrArrayConverter(bool canWrite) => CanWrite = canWrite;
public bool CanWrite { get; }
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
switch (reader.TokenType)
case JsonTokenType.StartArray:
var list = new TCollection();
if (reader.TokenType == JsonTokenType.EndArray)
list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
return new TCollection { JsonSerializer.Deserialize<TItem>(ref reader, options) };
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
if (CanWrite && value.Count == 1)
JsonSerializer.Serialize(writer, value.First(), options);
writer.WriteStartArray();
foreach (var item in value)
JsonSerializer.Serialize(writer, item, options);
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
public List<string> Category { get; set; }
namespace ConverterAppliedDirectly
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
public List<string> Category { get; set; }
namespace ObservableCollection
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
public static void Test()
TestObservableCollection();
TestConverterAppliedDirectly();
TestMiscellaneousCollections();
Assert.IsTrue(new SingleOrArrayConverterFactory().CanConvert(typeof(List<string>)));
Assert.IsTrue(!new SingleOrArrayConverterFactory().CanConvert(typeof(List<List<string>>)));
static void TestTruncatedJson()
var options = new JsonSerializerOptions
Converters = { new SingleOrArrayConverterFactory() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
for (int i = 2; i < json.Length-1; i++)
var badJson = json.Substring(0, json.Length - i);
Assert.Throws<JsonException>(() => JsonSerializer .Deserialize<List<Item>>(badJson, options));
var sampleBadJson = @"[{ ""category"": [ ""newuser"",";
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<List<Item>>(sampleBadJson, options));
static void TestObservableCollection()
var options = new JsonSerializerOptions
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
var list = JsonSerializer.Deserialize<List<ObservableCollection.Item>>(json, options);
var json2 = JsonSerializer.Serialize(list, options);
AssertEqualsJson(json, json2);
static void TestConverterAppliedDirectly()
var options = new JsonSerializerOptions
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
var list = JsonSerializer.Deserialize<List<ConverterAppliedDirectly.Item>>(json, options);
var json2 = JsonSerializer.Serialize(list, options);
AssertEqualsJson(json, json2);
static void TestInitial()
Assert.Throws<JsonException>(() => {
var options = new JsonSerializerOptions
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
var list = JsonSerializer.Deserialize<List<Item>>(json, options);
var json2 = JsonSerializer.Serialize(list, options);
static void TestMiscellaneousCollections()
JsonSerializer.Deserialize<HashSet<string>>(
new JsonSerializerOptions { Converters = { new SingleOrArrayConverter<HashSet<string>, string>() } })
JsonSerializer.Deserialize<Collection<string>>(
new JsonSerializerOptions { Converters = { new SingleOrArrayConverter<Collection<string>, string>() } })
JsonSerializer.Deserialize<ObservableCollection<string>>(
new JsonSerializerOptions { Converters = { new SingleOrArrayConverter<ObservableCollection<string>, string>() } })
static void TestFactory()
var options = new JsonSerializerOptions
Converters = { new SingleOrArrayConverterFactory() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
var list = JsonSerializer.Deserialize<List<Item>>(json, options);
var json2 = JsonSerializer.Serialize(list, options);
Console.WriteLine("Re-serialized {0}", list);
Console.WriteLine(json2);
AssertEqualsJson(json, json2);
foreach (var item in list)
var itemJson = JsonSerializer.Serialize(item, options);
var itemList = JsonSerializer.Deserialize<List<Item>>(itemJson, options);
Assert.IsTrue(itemList.Count == 1, "itemList.Count == 1");
var itemJson2 = JsonSerializer.Serialize(itemList, options);
AssertEqualsJson(itemJson, itemJson2);
static void AssertEqualsJson(string json1, string json2)
Assert.IsTrue(Newtonsoft.Json.Linq.JToken.DeepEquals(
Newtonsoft.Json.Linq.JToken.Parse(json1, new Newtonsoft.Json.Linq.JsonLoadSettings { CommentHandling = Newtonsoft.Json.Linq.CommentHandling.Ignore }),
Newtonsoft.Json.Linq.JToken.Parse(json2, new Newtonsoft.Json.Linq.JsonLoadSettings { CommentHandling = Newtonsoft.Json.Linq.CommentHandling.Ignore })
""email"": ""john.doe@sendgrid.com"",
""timestamp"": 1337966815,
""email"": ""jane.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
""email"": ""nul@sendgrid.com"",
""timestamp"": 1337966815,
},// Testing AllowTrailingCommas = true,
public static void Main()
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");
public static string GetNetCoreVersion()
var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
return assemblyPath[netCoreAppIndex + 1];