Skip to content

Commit

Permalink
Added inline array support (#4)
Browse files Browse the repository at this point in the history
* added inline json converter

* fixed readme

* added DTO tests
  • Loading branch information
Ilchert authored Jan 13, 2024
1 parent 6421928 commit 03176ae
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 27 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,26 @@ This code output:

```JSON
[1,"str"]
```
```

#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]"`

64 changes: 64 additions & 0 deletions src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -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<InlineArrayAttribute>() != null;

public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var attribute = typeToConvert.GetCustomAttribute<InlineArrayAttribute>();
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<TStruct, TItem>(int length, JsonSerializerOptions options) : JsonConverter<TStruct>
where TStruct : struct
{
private readonly JsonConverter<TItem?> _itemConverter = (JsonConverter<TItem?>)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<TStruct, TItem?>(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<TStruct, TItem?>(ref value), length);
for (var i = 0; i < span.Length; i++)
WriteValue(_itemConverter, writer, span[i], options);

writer.WriteEndArray();
}
}

}
#endif
35 changes: 35 additions & 0 deletions src/SystemTextJson.FluentApi/SerializationHelpers.cs
Original file line number Diff line number Diff line change
@@ -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<T>(JsonConverter<T?> 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<T>(JsonConverter<T?> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is null && !converter.HandleNull)
writer.WriteNullValue();
else
converter.Write(writer, value, options);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
<TargetFrameworks>net6.0;net7.0;netstandard2.0;net462;net8</TargetFrameworks>
<TargetFrameworks>net8.0;net6.0;net7.0;netstandard2.0;net462</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
Expand Down
26 changes: 1 addition & 25 deletions src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -58,22 +58,6 @@ public override TTuple Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
return result;
}

protected static T? ReadValue<T>(JsonConverter<T?> 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)
Expand All @@ -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<T>(JsonConverter<T?> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is null && !converter.HandleNull)
writer.WriteNullValue();
else
converter.Write(writer, value, options);
}
}

private class ValueTupleConverter<T1>(JsonSerializerOptions options) : ValueTupleConverterBase<ValueTuple<T1?>>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InlineArray>("[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<Dto>("{\"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;
}
}

0 comments on commit 03176ae

Please sign in to comment.