Skip to content

Instantly share code, notes, and snippets.

@shadeglare
Last active June 27, 2024 03:25
Show Gist options
  • Save shadeglare/86acd959ee69c3243a7a8f59ff75f848 to your computer and use it in GitHub Desktop.
Save shadeglare/86acd959ee69c3243a7a8f59ff75f848 to your computer and use it in GitHub Desktop.
Serialize and deserialize a generic union type with System.Text.Json
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Hexarc.Annotations;
namespace Hexarc.Union
{
public static class ResponseKind
{
public const String Ok = nameof(Ok);
public const String Error = nameof(Error);
}
[UnionTag(nameof(Kind))]
[UnionCase(typeof(OkResponse<>), ResponseKind.Ok)]
[UnionCase(typeof(ErrorResponse<>), ResponseKind.Error)]
public abstract class Response<T>
{
public abstract String Kind { get; }
}
public sealed class OkResponse<T> : Response<T>
{
public override String Kind => ResponseKind.Ok;
public T Data { get; init; } = default!;
}
public sealed class ErrorResponse<T> : Response<T>
{
public override String Kind => ResponseKind.Error;
}
public static class JsonExtension
{
public static Object? ToObject(this JsonElement element, Type type, JsonSerializerOptions options)
{
var bufferWriter = new ArrayBufferWriter<Byte>();
using (var writer = new Utf8JsonWriter(bufferWriter))
{
element.WriteTo(writer);
}
return JsonSerializer.Deserialize(bufferWriter.WrittenSpan, type, options);
}
public static Object? ToObject(this JsonDocument document, Type type, JsonSerializerOptions options)
{
if (document is null) throw new ArgumentNullException(nameof(document));
return document.RootElement.ToObject(type, options);
}
}
public static class TypeExtensions
{
public static Func<Type, Type> CreateConcreteTypeFactory(this Type type)
{
if (type.IsGenericType)
{
var genericArgs = type.GetGenericArguments();
return givenType => givenType.MakeGenericType(genericArgs);
}
else
{
return givenType => givenType;
}
}
}
public sealed class UnionConverter<T> : JsonConverter<T> where T : class
{
private String TagPropertyName { get; }
private Dictionary<String, Type> UnionTypes { get; }
public UnionConverter()
{
var type = typeof(T);
var unionTag = type.GetCustomAttribute<UnionTagAttribute>();
if (unionTag is null) throw new InvalidOperationException();
var concreteTypeFactory = type.CreateConcreteTypeFactory();
this.TagPropertyName = unionTag.TagPropertyName;
this.UnionTypes = type.GetCustomAttributes<UnionCaseAttribute>()
.ToDictionary(k => k.TagPropertyValue, e => concreteTypeFactory(e.CaseType));
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var document = JsonDocument.ParseValue(ref reader);
var propertyName = options.PropertyNamingPolicy?.ConvertName(this.TagPropertyName) ?? this.TagPropertyName;
var property = document.RootElement.GetProperty(propertyName);
var type = this.UnionTypes[property.GetString() ?? throw new InvalidOperationException()];
return (T?)document.ToObject(type, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
public sealed class UnionConverterFactory : JsonConverterFactory
{
public override Boolean CanConvert(Type typeToConvert) =>
typeToConvert.GetCustomAttribute<UnionTagAttribute>(false) is not null;
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var converterType = typeof(UnionConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter?)Activator.CreateInstance(converterType);
}
}
public static class Program
{
public static void Main()
{
var ok = new OkResponse<String>() { Data = "Box" };
var settings = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new UnionConverterFactory() }
};
Console.WriteLine(JsonSerializer.Serialize(ok, settings));
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Response<String>>(@"{""kind"": ""Ok"", ""data"": ""123""}", settings)));
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Response<String>?>(@"null", settings)));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment