You are here: Start » AVL.NET » JSON Serialization

JSON Serialization

  1. Introduction
  2. Converter for System.Text.Json
  3. Usage example

Introduction

Most of the simple Avl.NET types serializes to seamlessly to the JSON format. However, there are some exceptions and special cases to consider when serializing Atl.Optional<T> and Atl.Conditional<T> types. One would expect that both of these types would serialize to either the value they contain or to null if they do not contain a value. Instead, when they do not contain a value, serialization throws an InvalidOperationException exception.

Action action = () => JsonSerializer.Serialize(Conditional.Empty<Point2D>());
action.Should().Throw<InvalidOperationException>();

To resolve this, custom converters must be used.

Converter for System.Text.Json

The following example demonstrates a custom converter factory for handling both Atl.Optional<T> and Atl.Conditional<T> types when using System.Text.Json.JsonSerializer:

public class AtlNullableConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        if (!typeToConvert.IsGenericType)
            return false;

        return typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>) ||
            typeToConvert.GetGenericTypeDefinition() == typeof(Conditional<>);
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        Type valueType = typeToConvert.GetGenericArguments()[0];

        Type converterType = typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>)
            ? typeof(AtlOptionalConverter<>).MakeGenericType(valueType)
            : typeof(AtlConditionalConverter<>).MakeGenericType(valueType);

        return (JsonConverter?)Activator.CreateInstance(converterType);
    }

    private class AtlOptionalConverter<T> : JsonConverter<Optional<T>>
    {
        public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.Null &&
                JsonSerializer.Deserialize<T>(ref reader, options) is T value)
                return Optional.WithValue(value);
            else
                return Optional.Empty<T>();
        }

        public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options)
        {
            if (value.HasValue)
                JsonSerializer.Serialize(writer, value.Value, options);
            else
                writer.WriteNullValue();
        }
    }

    private class AtlConditionalConverter<T> : JsonConverter<Conditional<T>>
    {
        public override Conditional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.Null &&
                JsonSerializer.Deserialize<T>(ref reader, options) is T value)
                return Conditional.WithValue(value);
            else
                return Conditional.Empty<T>();
        }

        public override void Write(Utf8JsonWriter writer, Conditional<T> value, JsonSerializerOptions options)
        {
            if (value.HasValue)
                JsonSerializer.Serialize(writer, value.Value, options);
            else
                writer.WriteNullValue();
        }
    }
}

Usage example

With the custom converter factory in place, one can now serialize and deserialize Atl.Optional<T> and Atl.Conditional<T> types seamlessly. The following example demonstrates this usage:

JsonSerializerOptions options = new()
{
    Converters = { new AtlNullableConverterFactory() }
};

JsonSerializer
    .Serialize(
        Conditional.WithValue<Point2D>(new(1.5f, 2.5f)),
        options)
    .Should()
    .Be(@"{""X"":1.5,""Y"":2.5}");

JsonSerializer
    .Serialize(
        Optional.WithValue<Point2D>(new(1.5f, 2.5f)),
        options
    )
    .Should()
    .Be(@"{""X"":1.5,""Y"":2.5}");

JsonSerializer
    .Serialize(
        Conditional.Empty<Point2D>(),
        options
    )
    .Should()
    .Be("null");

var conditionalWithValue = JsonSerializer.Deserialize<Conditional<Point2D>>(@"{""X"":1.5,""Y"":2.5}", options);
conditionalWithValue.Should().NotBeNull();
conditionalWithValue.HasValue.Should().BeTrue();
conditionalWithValue.Value.Should().Be(new Point2D(1.5f, 2.5f));

var nullConditional = JsonSerializer.Deserialize<Conditional<Point2D>>("null", options);
nullConditional.Should().BeNull();
Previous: Settings example Next: Function Reference