diff --git a/README.md b/README.md index 25dd78f..940e7a7 100644 --- a/README.md +++ b/README.md @@ -211,4 +211,26 @@ This code output: ```JSON [1,"str"] -``` \ No newline at end of file +``` + +#Inline arrays support + +Fluent Api has `InlineArrayJsonConverter` for .NET 8 and above to serialize and deserialize [`InlineArray`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays) structs as arrays. + +```C# +var array = new InlineArray(); +array[0] = null; +array[1] = 1; +array[2] = -1; +var options = new JsonSerializerOptions() { Converters = { new InlineArrayJsonConverter() } }; +JsonSerializer.Serialize(array,options); + +[InlineArray(3)] +private struct InlineArray +{ + public int? Value; +} +``` + +Output: `"[null,1,-1]"` + diff --git a/src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs b/src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs new file mode 100644 index 0000000..e8a9084 --- /dev/null +++ b/src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs @@ -0,0 +1,64 @@ + +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SystemTextJson.FluentApi.SerializationHelpers; +namespace SystemTextJson.FluentApi; +#if NET8_0_OR_GREATER + +public class InlineArrayJsonConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) => + typeToConvert.GetCustomAttribute() != null; + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var attribute = typeToConvert.GetCustomAttribute(); + if (attribute is null) + return null; + + var length = attribute.Length; + var itemType = typeToConvert.GetFields()[0].FieldType; // inline array can have only one field + + var converterType = typeof(ConcreteInlineArrayJsonConverter<,>).MakeGenericType(typeToConvert, itemType); + return (JsonConverter)Activator.CreateInstance(converterType, length, options)!; + } + + private class ConcreteInlineArrayJsonConverter(int length, JsonSerializerOptions options) : JsonConverter + where TStruct : struct + { + private readonly JsonConverter _itemConverter = (JsonConverter)options.GetConverter(typeof(TItem)); + public override TStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + throw new JsonException("Start token must be '['."); + + reader.Read(); + + var result = default(TStruct); + var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref result), length); + for (var i = 0; i < span.Length; i++) + span[i] = ReadValue(_itemConverter, ref reader, options); + + if (reader.TokenType != JsonTokenType.EndArray) + throw new JsonException("Expected end token ']'."); + + return result; + } + + public override void Write(Utf8JsonWriter writer, TStruct value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref value), length); + for (var i = 0; i < span.Length; i++) + WriteValue(_itemConverter, writer, span[i], options); + + writer.WriteEndArray(); + } + } + +} +#endif diff --git a/src/SystemTextJson.FluentApi/SerializationHelpers.cs b/src/SystemTextJson.FluentApi/SerializationHelpers.cs new file mode 100644 index 0000000..c50b710 --- /dev/null +++ b/src/SystemTextJson.FluentApi/SerializationHelpers.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Threading.Tasks; + +namespace SystemTextJson.FluentApi; +internal class SerializationHelpers +{ + public static T? ReadValue(JsonConverter converter, ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var value = default(T); + if (reader.TokenType == JsonTokenType.Null && !converter.HandleNull) + { + if (value is not null) + throw new JsonException("Expected not null value."); + } + else + { + value = converter.Read(ref reader, typeof(T), options); + } + reader.Read(); + return value; + } + + public static void WriteValue(JsonConverter converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value is null && !converter.HandleNull) + writer.WriteNullValue(); + else + converter.Write(writer, value, options); + } +} diff --git a/src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj b/src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj index 3e37b98..a1eba9a 100644 --- a/src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj +++ b/src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj @@ -1,7 +1,7 @@ 1.0.0 - net6.0;net7.0;netstandard2.0;net462;net8 + net8.0;net6.0;net7.0;netstandard2.0;net462 enable enable 12 diff --git a/src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs b/src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs index 211b10d..3b7ca2f 100644 --- a/src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs +++ b/src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs @@ -1,6 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; - +using static SystemTextJson.FluentApi.SerializationHelpers; namespace SystemTextJson.FluentApi; public class ValueTupleJsonConverter : JsonConverterFactory { @@ -58,22 +58,6 @@ public override TTuple Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return result; } - protected static T? ReadValue(JsonConverter converter, ref Utf8JsonReader reader, JsonSerializerOptions options) - { - var value = default(T); - if (reader.TokenType == JsonTokenType.Null && !converter.HandleNull) - { - if (value is not null) - throw new JsonException("Expected not null value."); - } - else - { - value = converter.Read(ref reader, typeof(T), options); - } - reader.Read(); - return value; - } - protected internal abstract TTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); public override void Write(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options) @@ -84,14 +68,6 @@ public override void Write(Utf8JsonWriter writer, TTuple value, JsonSerializerOp } protected internal abstract void WriteTuple(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options); - - protected static void WriteValue(JsonConverter converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (value is null && !converter.HandleNull) - writer.WriteNullValue(); - else - converter.Write(writer, value, options); - } } private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> diff --git a/tests/SystemTextJson.FluentApi.Tests/InlineArrayJsonConverterTests.cs b/tests/SystemTextJson.FluentApi.Tests/InlineArrayJsonConverterTests.cs new file mode 100644 index 0000000..cf1fa5d --- /dev/null +++ b/tests/SystemTextJson.FluentApi.Tests/InlineArrayJsonConverterTests.cs @@ -0,0 +1,73 @@ +using Xunit; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace SystemTextJson.FluentApi.Tests; +public class InlineArrayJsonConverterTests +{ + readonly JsonSerializerOptions _options = new() { Converters = { new InlineArrayJsonConverter() } }; + + [Fact] + public void Write() + { + var array = new InlineArray(); + array[0] = null; + array[1] = 1; + array[2] = -1; + + var actualJson = JsonSerializer.Serialize(array, _options); + + var isEquals = JsonNode.DeepEquals(JsonNode.Parse("[null,1,-1]"), JsonNode.Parse(actualJson)); + Assert.True(isEquals, "Json not equal."); + } + + [Fact] + public void WriteDto() + { + var array = new InlineArray(); + array[0] = null; + array[1] = 1; + array[2] = -1; + var dto = new Dto() { Array = array }; + + var actualJson = JsonSerializer.Serialize(dto, _options); + + var isEquals = JsonNode.DeepEquals(JsonNode.Parse("{\"Array\":[null,1,-1]}"), JsonNode.Parse(actualJson)); + Assert.True(isEquals, "Json not equal."); + } + + [Fact] + public void Read() + { + var actual = JsonSerializer.Deserialize("[null,1,-1]", _options); + + Assert.Null(actual[0]); + Assert.Equal(actual[1], 1); + Assert.Equal(actual[2], -1); + } + + + [Fact] + public void ReadDto() + { + var actual = JsonSerializer.Deserialize("{\"Array\":[null,1,-1]}", _options); + + Assert.NotNull(actual); + var array = Assert.NotNull(actual.Array); + Assert.Null(array[0]); + Assert.Equal(array[1], 1); + Assert.Equal(array[2], -1); + } + + private class Dto + { + public InlineArray? Array { get; set; } + } + + [InlineArray(3)] + private struct InlineArray + { + public int? Value; + } +}