using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
public static class JsonExtensions
public static void AddNativePolymorphicTypInfo(JsonTypeInfo jsonTypeInfo)
if (typeof(ValueObject).IsAssignableFrom(jsonTypeInfo.Type) && !jsonTypeInfo.Type.IsSealed) {
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions {
TypeDiscriminatorPropertyName = "$mytype",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
.Where(t => !t.IsAbstract && jsonTypeInfo.Type.IsAssignableFrom(t));
foreach (var t in types.Select(t => new JsonDerivedType(t, t.Name.ToLowerInvariant())))
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(t);
public class MyBaseClass;
public class ValueObject;
public class MyValueObject : ValueObject {
public int Counter { get; set; }
public class MySubValueObject : MyValueObject {
public class RootObjectOne {
public ValueObject? ValueObject { get; set; }
public class RootObjectTwo {
public MyValueObject? ValueObject { get; set; }
private static JsonSerializerOptions BuildSerializerOptions() {
return new JsonSerializerOptions {
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.WithAddedModifier(JsonExtensions.AddNativePolymorphicTypInfo),
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
public static void Test1_Serialize_Only_MyValueObject_Declared_AsSelf() {
Console.WriteLine("TEST 1");
MyValueObject rootObject = new MyValueObject() { Counter = 5 };
var json = JsonSerializer.Serialize(rootObject, BuildSerializerOptions());
var expectedJson = "{\"$mytype\":\"myvalueobject\",\"counter\":5}";
Assert.That(expectedJson == json);
public static void Test2_Serialize_Only_MyValueObject_Declared_AsBase() {
Console.WriteLine("TEST 2");
ValueObject rootObject = new MyValueObject() { Counter = 5 };
var json = JsonSerializer.Serialize(rootObject, BuildSerializerOptions());
var expectedJson = "{\"$mytype\":\"myvalueobject\",\"counter\":5}";
Assert.That(expectedJson == json);
public static void Test3_Serialize_RootObjectOne_With_ValueObject_Declared_AsBase() {
Console.WriteLine("TEST 3");
var rootObject = new RootObjectOne { ValueObject = new MyValueObject() { Counter = 5 } };
var json = JsonSerializer.Serialize(rootObject, BuildSerializerOptions());
var expectedJson = "{\"valueObject\":{\"$mytype\":\"myvalueobject\",\"counter\":5}}";
Assert.That(expectedJson == json);
public static void Test4_Serialize_RootObjectTwo_With_ValueObject_Declared_AsSelf() {
Console.WriteLine("TEST 4");
var rootObject = new RootObjectTwo { ValueObject = new MyValueObject() { Counter = 5 } };
var json = JsonSerializer.Serialize(rootObject, BuildSerializerOptions());
var expectedJson = "{\"valueObject\":{\"$mytype\":\"myvalueobject\",\"counter\":5}}";
Assert.That(expectedJson == json);
public static void Test5_Serialize_MySubValueObject() {
Console.WriteLine("TEST 5");
var rootObject = new MySubValueObject { Counter = 5 };
var json = JsonSerializer.Serialize(rootObject, BuildSerializerOptions());
var expectedJson = "{\"$mytype\":\"mysubvalueobject\",\"counter\":5}";
Assert.That(expectedJson == json);
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
TestClass.Test1_Serialize_Only_MyValueObject_Declared_AsSelf();
Console.WriteLine("Failed with unhandled exception: ");
TestClass.Test2_Serialize_Only_MyValueObject_Declared_AsBase();
Console.WriteLine("Failed with unhandled exception: ");
TestClass.Test3_Serialize_RootObjectOne_With_ValueObject_Declared_AsBase();
Console.WriteLine("Failed with unhandled exception: ");
TestClass.Test4_Serialize_RootObjectTwo_With_ValueObject_Declared_AsSelf();
Console.WriteLine("Failed with unhandled exception: ");
TestClass.Test5_Serialize_MySubValueObject();
Console.WriteLine("Failed with unhandled exception: ");