I have a simple class hierarchy that I want to serialize using System.Text.Json.
There are 3 classes. The base is Shape. Inherited ones are Box and Circle.
I have a plan to use these classes as a tagged union on my frontend app so I just introduced a discriminator property Tag.
I wrote a type convertor that supports serialization/deserialization of this hierarchy.
What I'm trying to understand - is this a best approach to implement such functionality or not. Indeed the output result of serialization is quite ugly (I put a comment in an example below). I'm not sure it's done the best way anyway it's just working.
Here's the example how I implemented serialization/deserialization:
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Serialization.Theory
{
    public abstract class Shape
    {
        public abstract String Tag { get; }
    }
    public class Box : Shape
    {
        public override String Tag { get; } = nameof(Box);
        public Single Width { get; set; }
        public Single Height { get; set; }
        public override String ToString()
        {
            return $"{Tag}: Width={Width}, Height={Height}";
        }
    }
    public class Circle : Shape
    {
        public override String Tag { get; } = nameof(Circle);
        public Single Radius { get; set; }
        public override String ToString()
        {
            return $"{Tag}: Radius={Radius}";
        }
    }
    public class ShapeConverter : JsonConverter<Shape>
    {
        public override Boolean CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(Circle) || typeToConvert == typeof(Shape);
        }
        public override Shape Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var raw = reader.GetString();
            var doc = JsonDocument.Parse(raw);
            var prop = doc.RootElement.EnumerateObject().Where(x => x.Name == "Tag").First();
            var value = prop.Value.GetString();
            switch (value)
            {
                case nameof(Circle): 
                    return JsonSerializer.Deserialize<Circle>(raw);
                case nameof(Box):
                    return JsonSerializer.Deserialize<Box>(raw);
                default:
                    throw new NotSupportedException();
            }
        }
        public override void Write(Utf8JsonWriter writer, Shape value, JsonSerializerOptions options)
        {
            if (value is Circle circle)
            {
                writer.WriteStringValue(JsonSerializer.SerializeToUtf8Bytes(circle));
            }
            else if (value is Box box)
            {
                writer.WriteStringValue(JsonSerializer.SerializeToUtf8Bytes(box));
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Keep in base class references like it's a property on another object.
            Shape origin1 = new Box { Width = 10, Height = 20 };
            Shape origin2 = new Circle { Radius = 30 };
            var settings = new JsonSerializerOptions();
            settings.Converters.Add(new ShapeConverter());
            var raw1 = JsonSerializer.Serialize(origin1, settings);
            var raw2 = JsonSerializer.Serialize(origin2, settings);
            Console.WriteLine(raw1); // "{\u0022Tag\u0022:\u0022Box\u0022,\u0022Width\u0022:10,\u0022Height\u0022:20}"
            Console.WriteLine(raw2); // "{\u0022Tag\u0022:\u0022Circle\u0022,\u0022Radius\u0022:30}"
            var restored1 = JsonSerializer.Deserialize<Shape>(raw1, settings);
            var restored2 = JsonSerializer.Deserialize<Shape>(raw2, settings);
            Console.WriteLine(restored1); // Box: Width=10, Height=20
            Console.WriteLine(restored2); // Circle: Radius=30
        }
    }
}
 
     
     
    