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 Array2DConverter : JsonConverterFactory
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsArray && typeToConvert.GetArrayRank() == 2;
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) =>
(JsonConverter)Activator.CreateInstance(
typeof(Array2DConverterInner<>).MakeGenericType(new [] { type.GetElementType() }),
BindingFlags.Instance | BindingFlags.Public,
args: new object[] { options },
class Array2DConverterInner<T> : JsonConverter<T [,]>
readonly JsonConverter<T> _valueConverter;
public Array2DConverterInner(JsonSerializerOptions options) =>
this._valueConverter = (typeof(T) == typeof(object) ? null : (JsonConverter<T>)options.GetConverter(typeof(T)));
public override void Write(Utf8JsonWriter writer, T [,] array, JsonSerializerOptions options)
var rowsFirstIndex = array.GetLowerBound(0);
var rowsLastIndex = array.GetUpperBound(0);
var columnsFirstIndex = array.GetLowerBound(1);
var columnsLastIndex = array.GetUpperBound(1);
writer.WriteStartArray();
for (var i = rowsFirstIndex; i <= rowsLastIndex; i++)
writer.WriteStartArray();
for (var j = columnsFirstIndex; j <= columnsLastIndex; j++)
_valueConverter.WriteOrSerialize(writer, array[i, j], options);
public override T [,] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
JsonSerializer.Deserialize<List<List<T>>>(ref reader, options)?.To2D();
public static partial class ArrayExtensions
public static T[,] To2D<T>(this List<List<T>> source)
var firstDim = source.Count;
var secondDim = source.Select(row => row.Count).FirstOrDefault();
if (!source.All(row => row.Count == secondDim))
throw new InvalidOperationException();
var result = new T[firstDim, secondDim];
for (var i = 0; i < firstDim; i++)
for (int j = 0, count = source[i].Count; j < count; j++)
result[i, j] = source[i][j];
public static class JsonSerializerExtensions
public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
converter.Write(writer, value, options);
JsonSerializer.Serialize(writer, value, typeof(T), options);
public static void Test()
var array1 = new double [,] { {1.1, 1.2, 1.3 }, { 2.1, 2.2, 2.3 } };
Test(new object [,] { {"1.1", 1.2, 1.3 }, { 2, 3, 4 } } );
var nonZeroBasedArray = (int [,])Array.CreateInstance(typeof(int), new[] { 2, 2 }, new[] { 100, 100 });
nonZeroBasedArray[100,100] = 1;
nonZeroBasedArray[100,101] = 2;
nonZeroBasedArray[101,100] = 3;
nonZeroBasedArray[101,101] = 4;
static void Test<T>(T [,] array1)
var options = new JsonSerializerOptions
Converters = { new Array2DConverter() },
var json = JsonSerializer.Serialize(array1, options);
var array2 = JsonSerializer.Deserialize<T [,]>(json, options);
if (typeof(T) != typeof(object))
Assert.IsTrue(array1.Equal2D(array2));
var newtonsoftJson = Newtonsoft.Json.JsonConvert.SerializeObject(array1);
Assert.AreEqual(json, newtonsoftJson);
var json2 = JsonSerializer.Serialize(array2, options);
Assert.AreEqual(json, json2);
public static partial class ArrayExtensions
public static bool Equal2D<T>(this T [,] array1, T [,] array2) =>
array1.Rank == array2.Rank
&& Enumerable.Range(0, array1.Rank).All(dimension => array1.GetLength(dimension) == array2.GetLength(dimension))
&& array1.Cast<T>().SequenceEqual(array2.Cast<T>());
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.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
return assemblyPath[netCoreAppIndex + 1];