using System.Collections;
using System.Collections.Generic;
using System.Text.Json.Serialization;
public class Jagged2DOr1DListConverter : JsonConverterFactory
public virtual bool CanWrite => true;
public override bool CanConvert(Type typeToConvert) => Get2DListItemType(typeToConvert) != null;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
var itemType = Get2DListItemType(typeToConvert)!;
var converterType = typeof(Jagged2DOr1DListConverter<>).MakeGenericType(itemType);
return (JsonConverter)Activator.CreateInstance(converterType, new object [] { CanWrite })!;
static Type? Get2DListItemType(Type type)
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(List<>))
var midType = type.GetGenericArguments()[0];
if (!midType.IsGenericType || midType.GetGenericTypeDefinition() != typeof(List<>))
var itemType = midType.GetGenericArguments()[0];
if (itemType == typeof(string) || itemType == typeof(byte []))
if (itemType == typeof(object))
if (typeof(IEnumerable).IsAssignableFrom(itemType))
public class Jagged2DOr1DListReadOnlyConverter : Jagged2DOr1DListConverter
public override bool CanWrite => false;
public class Jagged2DOr1DListConverter<TItem> : JsonConverter<List<List<TItem>>>
public Jagged2DOr1DListConverter() : this(true) {}
public Jagged2DOr1DListConverter(bool canWrite) => CanWrite = canWrite;
public bool CanWrite { get; }
public override List<List<TItem>>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
if (reader.TokenType == JsonTokenType.Null)
else if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException();
List<List<TItem>> outerList = new();
List<TItem>? innerList = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
if (reader.TokenType == JsonTokenType.StartArray)
if (outerList.Count > 0 && innerList != null)
throw new JsonException();
var itemList = JsonSerializer.Deserialize<List<TItem>>(ref reader, options);
outerList.Add(itemList!);
throw new JsonException();
outerList.Add(innerList = new ());
innerList.Add(JsonSerializer.Deserialize<TItem>(ref reader, options)!);
public override void Write(Utf8JsonWriter writer, List<List<TItem>> 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 List<List<decimal>> chart { get; set; } = new ();
public Data data { get; set; } = new();
public static void Test()
foreach (var json in GetJson())
static void TestCanConvert()
var converter = new Jagged2DOr1DListConverter();
Assert.That(converter.CanConvert(typeof(List<List<string>>)));
Assert.That(converter.CanConvert(typeof(List<List<byte []>>)));
Assert.That(!converter.CanConvert(typeof(List<List<object>>)));
Assert.That(converter.CanConvert(typeof(List<List<Root>>)));
Assert.That(!converter.CanConvert(typeof(List<List<string []>>)));
Assert.That(!converter.CanConvert(typeof(List<List<byte [][]>>)));
Assert.That(!converter.CanConvert(typeof(List<List<object []>>)));
Assert.That(!converter.CanConvert(typeof(List<List<List<string>>>)));
Assert.That(!converter.CanConvert(typeof(List<List<List<byte>>>)));
Assert.That(!converter.CanConvert(typeof(List<List<List<object>>>)));
static void TestReadWrite(string json)
var options = new JsonSerializerOptions
Converters = { new Jagged2DOr1DListConverter() },
var root = JsonSerializer.Deserialize<Root>(json, options);
var newJson = JsonSerializer.Serialize(root, options);
Console.WriteLine("Re-serialized {0}:", root);
Console.WriteLine(newJson);
AssertEqualsJson(json, newJson);
var root2 = JsonSerializer.Deserialize<Root>(newJson, options);
Assert.AreEqual(root?.data?.chart?.Count, root2?.data?.chart?.Count);
Assert.AreEqual(root?.data?.chart?.SelectMany(i => i)?.Count(), root2?.data?.chart?.SelectMany(i => i)?.Count());
static void TestReadOnly(string json)
var options = new JsonSerializerOptions
Converters = { new Jagged2DOr1DListReadOnlyConverter() },
var root = JsonSerializer.Deserialize<Root>(json, options);
var newJson = JsonSerializer.Serialize(root, options);
Console.WriteLine("Re-serialized {0}:", root);
Console.WriteLine(newJson);
var root2 = JsonSerializer.Deserialize<Root>(newJson, options);
Assert.AreEqual(root?.data?.chart?.Count, root2?.data?.chart?.Count);
Assert.AreEqual(root?.data?.chart?.SelectMany(i => i)?.Count(), root2?.data?.chart?.SelectMany(i => i)?.Count());
static void TestTruncatedJson(string json)
var options = new JsonSerializerOptions
Converters = { new Jagged2DOr1DListConverter() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
for (int i = 2; i < json.Length-1; i++)
var badJson = json.Substring(0, json.Length - i);
var ex = Assert.Throws<JsonException>(() => JsonSerializer .Deserialize<Root>(badJson, options));
Console.WriteLine(ex.Message);
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 })
static IEnumerable<string> GetJson() => new []
{"data" : { "chart": [ 1, 1 ] } }
{"data": {"chart": [ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ]]}}
public static void Main()
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");