Is polymorphic deserialization possible in System.Text.Json? - c#

I try to migrate from Newtonsoft.Json to System.Text.Json.
I want to deserialize abstract class. Newtonsoft.Json has TypeNameHandling for this.
Is there any way to deserialize abstract class via System.Text.Json on .net core 3.0?

Is polymorphic deserialization possible in System.Text.Json?
The answer is yes and no, depending on what you mean by "possible".
There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).
Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.
However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.
The docs show an example of how to do that using a type discriminator property:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization
Let's look at an example.
Say you have a base class and a couple of derived classes:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
}
You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):
private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };
// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};
string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/
var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);
// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/
var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);
Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}
Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism:
Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

I ended up with that solution. It's lightwight and a generic enough for me.
The type discriminator converter
public class TypeDiscriminatorConverter<T> : JsonConverter<T> where T : ITypeDiscriminator
{
private readonly IEnumerable<Type> _types;
public TypeDiscriminatorConverter()
{
var type = typeof(T);
_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty(nameof(ITypeDiscriminator.TypeDiscriminator), out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T) JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
The interface
public interface ITypeDiscriminator
{
string TypeDiscriminator { get; }
}
And the example models
public interface ISurveyStepResult : ITypeDiscriminator
{
string Id { get; set; }
}
public class BoolStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(BoolStepResult);
public bool Value { get; set; }
}
public class TextStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(TextStepResult);
public string Value { get; set; }
}
public class StarsStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(StarsStepResult);
public int Value { get; set; }
}
And here is the test method
public void SerializeAndDeserializeTest()
{
var surveyResult = new SurveyResultModel()
{
Id = "id",
SurveyId = "surveyId",
Steps = new List<ISurveyStepResult>()
{
new BoolStepResult(){ Id = "1", Value = true},
new TextStepResult(){ Id = "2", Value = "some text"},
new StarsStepResult(){ Id = "3", Value = 5},
}
};
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = { new TypeDiscriminatorConverter<ISurveyStepResult>()},
WriteIndented = true
};
var result = JsonSerializer.Serialize(surveyResult, jsonSerializerOptions);
var back = JsonSerializer.Deserialize<SurveyResultModel>(result, jsonSerializerOptions);
var result2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
Assert.IsTrue(back.Steps.Count == 3
&& back.Steps.Any(x => x is BoolStepResult)
&& back.Steps.Any(x => x is TextStepResult)
&& back.Steps.Any(x => x is StarsStepResult)
);
Assert.AreEqual(result2, result);
}

Please try this library I wrote as an extension to System.Text.Json to offer polymorphism:
https://github.com/dahomey-technologies/Dahomey.Json
If the actual type of a reference instance differs from the declared type, the discriminator property will be automatically added to the output json:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
}
public class WeatherForecastDerived : WeatherForecast
{
public int WindSpeed { get; set; }
}
Inherited classes must be manually registered to the discriminator convention registry in order to let the framework know about the mapping between a discriminator value and a type:
JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();
DiscriminatorConventionRegistry registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterType<WeatherForecastDerived>();
string json = JsonSerializer.Serialize<WeatherForecast>(weatherForecastDerived, options);
Result:
{
"$type": "Tests.WeatherForecastDerived, Tests",
"Date": "2019-08-01T00:00:00-07:00",
"TemperatureCelsius": 25,
"Summary": "Hot",
"WindSpeed": 35
}

Polymorphic serialization of whitelisted inherited types has been implemented in .NET 7, and is available in Preview 6.
From the documentation page What’s new in System.Text.Json in .NET 7: Type Hierarchies:
System.Text.Json now supports polymorphic serialization and deserialization of user-defined type hierarchies. This can be enabled by decorating the base class of a type hierarchy with the new JsonDerivedTypeAttribute.
First, let's consider serialization. Say you have the following type hierarchy:
public abstract class BaseType { } // Properties omitted
public class DerivedType1 : BaseType { public string Derived1 { get; set; } }
public class DerivedType2 : BaseType { public int Derived2 { get; set; } }
And you have a data model that includes a value whose declared type is BaseType, e.g.
var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };
In previous versions, System.Text.Json would only serialize the properties of the declared type BaseType. Now you will be able to include the properties of DerivedType1 when serializing a value declared as BaseType by adding [JsonDerivedType(typeof(TDerivedType))] to BaseType for all derived types:
[JsonDerivedType(typeof(DerivedType1))]
[JsonDerivedType(typeof(DerivedType2))]
public abstract class BaseType { } // Properties omitted
Having whitelisted DerivedType1 in this manner, serialization of your model:
var json = JsonSerializer.Serialize(list);
Results in
[{"Derived1" : "value 1"}]
Demo fiddle #1 here.
Do note that only derived types whitelisted via attribute (or through setting JsonTypeInfo.PolymorphismOptions in runtime) can be serialized via this mechanism. If you have some other derived type which is not whitelisted, e.g.:
public class DerivedType3 : BaseType { public string Derived3 { get; set; } }
Then JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } }) will throw a System.NotSupportedException: Runtime type 'DerivedType3' is not supported by polymorphic type 'BaseType' exception. Demo fiddle #2 here.
That covers serialization. If you need to round-trip your type hierarchy, you will need to supply a type discriminator property value to use for each derived type. This may be done providing a value for JsonDerivedTypeAttribute.TypeDiscriminator for each derived type:
[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted
Now when you serialize your model
var json = JsonSerializer.Serialize(list);
System.Text.Json will add an artificial type discriminator property "$type" indicating the type that was serialized:
[{"$type" : "DerivedType1", "Derived1" : "value 1"}]
Having done so, you can now deserialize your data model like so:
var list2 = JsonSerializer.Deserialize<List<BaseType>>(json);
And the actual, concrete type(s) serialized will be preserved. Demo fiddle #3 here.
It is also possible to inform System.Text.Json of your type hierarchy in runtime via Contract Customization. You might need to do this when your type hierarchy cannot be modified, or when some derived types are in different assemblies and cannot be referenced at compile time, or you are trying to interoperate between multiple legacy serializers. The basic workflow here will be to instantiate an instance of DefaultJsonTypeInfoResolver and add a modifier which sets up the necessary PolymorphismOptions for the JsonTypeInfo for your base type.
For example, polymorphic serialization for the BaseType hierarchy can be enabled in runtime like so:
var resolver = new DefaultJsonTypeInfoResolver
{
Modifiers =
{
// Add an Action<JsonTypeInfo> modifier that sets up the polymorphism options for BaseType
static typeInfo =>
{
if (typeInfo.Type != typeof(BaseType))
return;
typeInfo.PolymorphismOptions = new()
{
DerivedTypes =
{
new JsonDerivedType(typeof(DerivedType1), "Derived1"),
new JsonDerivedType(typeof(DerivedType2), "Derived2")
}
};
},
// Add other modifiers as required.
}
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = resolver,
// Add other options as required
};
var json = JsonSerializer.Serialize(list, options);
Demo fiddle #4 here.
Notes:
The whitelisting approach is consistent with the approach of the data contract serializers, which use the KnownTypeAttribute, and XmlSerializer, which uses XmlIncludeAttribute. It is inconsistent with Json.NET, whose TypeNameHandling serializes type information for all types unless explicitly filtered via a serialization binder.
Allowing only whitelisted types to be deserialized prevents Friday the 13th: JSON Attacks type injection attacks including those detailed in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.
Integers as well as strings may be used for the type discriminator name. If you define your type hierarchy as follows:
[JsonDerivedType(typeof(DerivedType1), 1)]
[JsonDerivedType(typeof(DerivedType2), 2)]
public abstract class BaseType { } // Properties omitted
Then serializing the list above results in
[{"$type" : 1, "Derived1" : "value 1"}]
Numeric type discriminator values are not used by Newtonsoft however, so if you are interoperating with a legacy serializer you might want to avoid this.
The default type discriminator property name, "$type", is the same type discriminator name used by Json.NET. If you would prefer to use a different property name, such as the name "__type" used by DataContractJsonSerializer, apply JsonPolymorphicAttribute to the base type and set TypeDiscriminatorPropertyName like so:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]
[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted
If you are interoperating with Json.NET (or DataContractJsonSerializer), you may set the value of TypeDiscriminator equal to the type discriminator value used by the legacy serializer.
If the serializer encounters a derived type that has not been whitelisted, you can control its behavior by setting JsonPolymorphicAttribute.UnknownDerivedTypeHandling to one of the following values:
JsonUnknownDerivedTypeHandling
Value
Meaning
FailSerialization
0
An object of undeclared runtime type will fail polymorphic serialization.
FallBackToBaseType
1
An object of undeclared runtime type will fall back to the serialization contract of the base type.
FallBackToNearestAncestor
2
An object of undeclared runtime type will revert to the serialization contract of the nearest declared ancestor type. Certain interface hierarchies are not supported due to diamond ambiguity constraints.

Thats my JsonConverter for all abstract types:
private class AbstractClassConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("JsonTokenType.StartObject not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type")
throw new JsonException("Property $type not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
throw new JsonException("Value at $type is invalid.");
string assemblyQualifiedName = reader.GetString();
var type = Type.GetType(assemblyQualifiedName);
using (var output = new MemoryStream())
{
ReadObject(ref reader, output, options);
return JsonSerializer.Deserialize(output.ToArray(), type, options);
}
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
{
Encoder = options.Encoder,
Indented = options.WriteIndented
}))
{
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if(objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString());
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var valueType = value.GetType();
var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth
}))
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
jsonProperty.WriteTo(writer);
}
writer.WriteEndObject();
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
}

I really liked the answer of Demetrius, but I think you can go even further in terms of re-usability. I came up with the following solution:
The JsonConverterFactory:
/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverter{T}"/>
/// </summary>
public class AbstractClassConverterFactory
: JsonConverterFactory
{
/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> containing the mappings of types to their respective <see cref="JsonConverter"/>
/// </summary>
protected static Dictionary<Type, JsonConverter> Converters = new Dictionary<Type, JsonConverter>();
/// <summary>
/// Initializes a new <see cref="AbstractClassConverterFactory"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverterFactory(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert.IsAbstract && typeToConvert.IsDefined(typeof(DiscriminatorAttribute));
}
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if(!Converters.TryGetValue(typeToConvert, out JsonConverter converter))
{
Type converterType = typeof(AbstractClassConverter<>).MakeGenericType(typeToConvert);
converter = (JsonConverter)Activator.CreateInstance(converterType, this.NamingPolicy);
Converters.Add(typeToConvert, converter);
}
return converter;
}
}
The JsonConverter:
/// <summary>
/// Represents the <see cref="JsonConverter"/> used to convert to/from an abstract class
/// </summary>
/// <typeparam name="T">The type of the abstract class to convert to/from</typeparam>
public class AbstractClassConverter<T>
: JsonConverter<T>
{
/// <summary>
/// Initializes a new <see cref="AbstractClassConverter{T}"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverter(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
DiscriminatorAttribute discriminatorAttribute = typeof(T).GetCustomAttribute<DiscriminatorAttribute>();
if (discriminatorAttribute == null)
throw new NullReferenceException($"Failed to find the required '{nameof(DiscriminatorAttribute)}'");
this.DiscriminatorProperty = typeof(T).GetProperty(discriminatorAttribute.Property, BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance);
if (this.DiscriminatorProperty == null)
throw new NullReferenceException($"Failed to find the specified discriminator property '{discriminatorAttribute.Property}' in type '{typeof(T).Name}'");
this.TypeMappings = new Dictionary<string, Type>();
foreach (Type derivedType in TypeCacheUtil.FindFilteredTypes($"nposm:json-polymorph:{typeof(T).Name}",
(t) => t.IsClass && !t.IsAbstract && t.BaseType == typeof(T)))
{
DiscriminatorValueAttribute discriminatorValueAttribute = derivedType.GetCustomAttribute<DiscriminatorValueAttribute>();
if (discriminatorValueAttribute == null)
continue;
string discriminatorValue = null;
if (discriminatorValueAttribute.Value.GetType().IsEnum)
discriminatorValue = EnumHelper.Stringify(discriminatorValueAttribute.Value, this.DiscriminatorProperty.PropertyType);
else
discriminatorValue = discriminatorValueAttribute.Value.ToString();
this.TypeMappings.Add(discriminatorValue, derivedType);
}
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <summary>
/// Gets the discriminator <see cref="PropertyInfo"/> of the abstract type to convert
/// </summary>
protected PropertyInfo DiscriminatorProperty { get; }
/// <summary>
/// Gets an <see cref="Dictionary{TKey, TValue}"/> containing the mappings of the converted type's derived types
/// </summary>
protected Dictionary<string, Type> TypeMappings { get; }
/// <inheritdoc/>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Start object token type expected");
using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
{
string discriminatorPropertyName = this.NamingPolicy?.ConvertName(this.DiscriminatorProperty.Name);
if (!jsonDocument.RootElement.TryGetProperty(discriminatorPropertyName, out JsonElement discriminatorProperty))
throw new JsonException($"Failed to find the required '{this.DiscriminatorProperty.Name}' discriminator property");
string discriminatorValue = discriminatorProperty.GetString();
if (!this.TypeMappings.TryGetValue(discriminatorValue, out Type derivedType))
throw new JsonException($"Failed to find the derived type with the specified discriminator value '{discriminatorValue}'");
string json = jsonDocument.RootElement.GetRawText();
return (T)JsonSerializer.Deserialize(json, derivedType);
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
The DiscriminatorAttribute:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the property used to discriminate derived types of the marked class
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorAttribute"/>
/// </summary>
/// <param name="property">The name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/></param>
public DiscriminatorAttribute(string property)
{
this.Property = property;
}
/// <summary>
/// Gets the name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/>
/// </summary>
public string Property { get; }
}
The DiscriminatorValueAttribute:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the discriminator value of a derived type
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorValueAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorValueAttribute"/>
/// </summary>
/// <param name="value">The value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/></param>
public DiscriminatorValueAttribute(object value)
{
this.Value = value;
}
/// <summary>
/// Gets the value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/>
/// </summary>
public object Value { get; }
}
And finally, an example of how to use it on classes:
[Discriminator(nameof(Type))]
public abstract class Identity
{
public virtual IdentityType Type { get; protected set; }
}
[DiscriminatorValue(IdentityType.Person)]
public class Person
: Identity
{
}
And... Voilà!
All that is left to do is to register the factory:
this.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
});

Throwing this option out there: Using a source code generator to generate a JsonConverter automatically for objects with a property marked with a special attribute
You can try it with this package, but it requires .net5
https://github.com/wivuu/Wivuu.JsonPolymorphism
The generator looks at the type of the property marked with a discriminator attribute, and then looks for types inheriting from the type holding the discriminator to match up with each case of the enum
Source here: https://github.com/wivuu/Wivuu.JsonPolymorphism/blob/master/Wivuu.JsonPolymorphism/JsonConverterGenerator.cs
enum AnimalType
{
Insect,
Mammal,
Reptile,
Bird // <- This causes an easy to understand build error if it's missing a corresponding inherited type!
}
// My base type is 'Animal'
abstract partial record Animal( [JsonDiscriminator] AnimalType type, string Name );
// Animals with type = 'Insect' will automatically deserialize as `Insect`
record Insect(int NumLegs = 6, int NumEyes=4) : Animal(AnimalType.Insect, "Insectoid");
record Mammal(int NumNipples = 2) : Animal(AnimalType.Mammal, "Mammalian");
record Reptile(bool ColdBlooded = true) : Animal(AnimalType.Reptile, "Reptilian");

I want to throw in another implementation suitable for hierarchical, secure, bi-directional, generic usage.
The following caveats
It is a performance and memory "nightmare" but good enough for most scenarios (why: because you need to read ahead $type and then would need to go back on the reader).
It works only if the polymorphic base is abstract / never serialized as instance itself (why: because otherwise the regular converter cannot work on the derived classes as it goes into stack overflow).
Works under .NET 6 ... will not in 3.1.
Example
public abstract record QueryClause(); // the abstract is kind of important
public record AndClause(QueryClause[] SubClauses) : QueryClause();
public record OrClause(QueryClause[] SubClauses) : QueryClause();
// ...
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new BaseClassConverter<QueryClause>(
typeof(AndClause),
typeof(OrClause)));
// ...
Converter
public class BaseClassConverter<TBaseType> : JsonConverter<TBaseType>
where TBaseType : class
{
private readonly Type[] _types;
private const string TypeProperty = "$type";
public BaseClassConverter(params Type[] types)
{
_types = types;
}
public override bool CanConvert(Type type)
=> typeof(TBaseType) == type; // only responsible for the abstract base
public override TBaseType Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
TBaseType result;
if (JsonDocument.TryParseValue(ref reader, out var doc))
{
if (doc.RootElement.TryGetProperty(TypeProperty, out var typeProperty))
{
var typeName = typeProperty.GetString();
var type = _types.FirstOrDefault(t => t.Name == typeName) ?? throw new JsonException($"{TypeProperty} specifies an invalid type");
var rootElement = doc.RootElement.GetRawText();
result = JsonSerializer.Deserialize(rootElement, type, options) as TBaseType ?? throw new JsonException("target type could not be serialized");
}
else
{
throw new JsonException($"{TypeProperty} missing");
}
}
else
{
throw new JsonException("Failed to parse JsonDocument");
}
return result;
}
public override void Write(
Utf8JsonWriter writer,
TBaseType value,
JsonSerializerOptions options)
{
var type = value.GetType();
if (_types.Any(t => type.Name == t.Name))
{
var jsonElement = JsonSerializer.SerializeToElement(value, type, options);
var jsonObject = JsonObject.Create(jsonElement) ?? throw new JsonException();
jsonObject[TypeProperty] = type.Name;
jsonObject.WriteTo(writer, options);
}
else
{
throw new JsonException($"{type.Name} with matching base type {typeof(TBaseType).Name} is not registered.");
}
}
}
If you find something, shoot me a comment.
Some kudos to 1.

currently with new feature of .NET 7 we can do this without write handy codes to implement this.
see here: https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-5/
[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }
JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }
i hope this can help you

I changed a couple things based on ahsonkhan's answer.
Personally I like this way since the client can just give their object to the server.
However, the 'Type' property must be first in the object.
Base class and derived classes:
public interface IBaseClass
{
public DerivedType Type { get; set; }
}
public class DerivedA : IBaseClass
{
public DerivedType Type => DerivedType.DerivedA;
public string Str { get; set; }
}
public class DerivedB : IBaseClass
{
public DerivedType Type => DerivedType.DerivedB;
public bool Bool { get; set; }
}
private enum DerivedType
{
DerivedA = 0,
DerivedB = 1
}
You can create JsonConverter<IBaseClass> that reads and checks the 'Type' property while serializing. It will use that to figure out which type to deserialize.
The reader has to be copied since we read the first property as the type. And then we have to read the full object again (pass it to the Deserialize method).
public class BaseClassConverter : JsonConverter<IBaseClass>
{
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IBaseClass baseClass = default;
DerivedType type= (DerivedType)reader.GetInt32();
switch (type)
{
case DerivedType.DerivedA:
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case DerivedType.DerivedB:
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
IBaseClass value,
JsonSerializerOptions options)
{
switch(value)
{
case DerivedA derivedA:
JsonSerializer.Serialize(writer, derivedA, options);
break;
case DerivedB derivedB:
JsonSerializer.Serialize(writer, derivedB, options);
break;
default:
throw new NotSupportedException();
}
}
}
The client is now able to send objects as follows:
// DerivedA
{
"Type": 0,
"Str": "Hello world!"
}
// DerivedB
{
"Type": 1,
"Bool": false
}
EDIT:
Edited the Read method to be able to deal with the property name not being in the first order. Now it just reads through the json and stops until it finds the 'Type' property name
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (typeReader.Read())
{
if (typeReader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string propertyName = typeReader.GetString();
if (propertyName.Equals(nameof(IBaseClass.Type)))
{
break;
}
typeReader.Skip();
}
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IGraphOptions baseClass = default;
GraphType type = (GraphType)typeReader.GetInt32();
....
// The switch..
....
To be honest, I think the way this custom System.Text JsonConverter is set up is unneccesary complex and I prefer the Newtonsoft JsonConverter.

Basing on the accepted answer, but using KnownTypeAttribute to discover the types (often enumerating all types can lead to unwanted type load exceptions) , and adding the discriminator property in the converter instead of having the class implement it itself:
public class TypeDiscriminatorConverter<T> : JsonConverter<T>
{
private readonly IEnumerable<Type> _types;
public TypeDiscriminatorConverter()
{
var type = typeof(T);
var knownTypes = type.GetCustomAttributes(typeof(KnownTypeAttribute), false).OfType<KnownTypeAttribute>();
_types = knownTypes.Select(x => x.Type).ToArray();
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty("discriminator",
out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.FullName == typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T)JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(value)))
{
writer.WritePropertyName("discriminator");
writer.WriteStringValue(value.GetType().FullName);
foreach (var property in document.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}
which you can use like this:
[JsonConverter(typeof(JsonInheritanceConverter))]
[KnownType(typeof(DerivedA))]
[KnownType(typeof(DerivedB))]
public abstract class BaseClass
{
//..
}

Don't write like this
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
If you class contain baseClass property then you deserialize him like baseClass.
If you baseClass is abstract and contain baseClass property then you got Exception.
It's safer to write like this:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
public BaseClass derived { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
public BaseClass derived { get; set; }
}
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass) == type;
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA), options);
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB), options);
break;
case TypeDiscriminator.BaseClass:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader, typeof(BaseClass));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA, options);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB, options);
}
else if (value is BaseClass baseClass)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, baseClass);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
But you BaseClass don't must contain property with type BaseClass or inheritor.

For interface property deserialization I've created a simple StaticTypeMapConverter
public class StaticTypeMapConverter<SourceType, TargetType> : JsonConverter<SourceType>
where SourceType : class
where TargetType : class, new()
{
public override SourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (SourceType)JsonSerializer.Deserialize(jsonObject, typeof(TargetType), options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, SourceType value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
You can use it like this:
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = {
new StaticTypeMapConverter<IMyInterface, MyImplementation>(),
new StaticTypeMapConverter<IMyInterface2, MyInterface2Class>(),
},
WriteIndented = true
};
var config = JsonSerializer.Deserialize<Config>(configContentJson, jsonSerializerOptions);

Not very elegant or efficient, but quick to code for a small number of child types:
List<Dictionary<string, object>> generics = JsonSerializer.Deserialize<List<Dictionary<string, object>>>(json);
List<InputOutputInstanceDto> result = new List<ParentType>();
foreach (Dictionary<string, object> item in generics)
{
switch(item["dataType"]) // use whatever field is in your parent/interface
{
case "Type1":
result.Add(JsonSerializer.Deserialize<Type1>(
JsonSerializer.Serialize(item)));
break
// add cases for each child type supported
default:
result.Add(JsonSerializer.Deserialize<ParentType>(
JsonSerializer.Serialize(item)));
break;
}
}

I like to share with you an issue I found using System.Text.Json. I followed the approach TypeDiscriminatorConverter that Demetrius Axenowski. It works very well.
My problems started when I added some annotations for the JSON. For example:
[JsonPropertyName("name")]
I have lost all day to understand why the code didn't work. I created some dummy code to understand where the problem was. All the source code is now on GitHub.
So, the problem was in the JsonPropertyName for the property I check in the converter. For example, this is a class
public class Radiobutton : ElementBase
{
[JsonPropertyName("type")]
public string Type => "Radiobutton";
public ElementType ElementType = ElementType.Radiobutton;
public List<string>? Choices { get; set; }
}
As you can see, I set the JsonPropertyName because I like to see type in lower case. Now, if I convert the class with this converter:
public class ElementTypeConverter<T> : JsonConverter<T> where T : IElementType
{
private readonly IEnumerable<Type> _types;
public ElementTypeConverter()
{
var type = typeof(T);
_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
}
public override T Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty(
nameof(IElementType.Type), out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.Name ==
typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T)JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value,
JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
I get the following error:
Test method SurveyExampleNetStardard21.Tests.UnitTest1.TestConversionJson_SystemTextJson_3Textbox_1radiobutton threw exception:
System.Text.Json.JsonException: The JSON value could not be converted to System.Collections.Generic.List`1[SurveyExampleNetStardard21.Interfaces.IElement]. Path: $.Elements[3] | LineNumber: 42 | BytePositionInLine: 5.
I removed the JsonPropertyName and it works fine. I tried to set
[JsonPropertyName("Type")]
(basically, the same as the variable) and it works fine. So, don't change the name. The converter is working both ways (object to Json and Json to object). This is the test code:
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = { new ElementTypeConverter<IElement>() },
WriteIndented = true
};
var json = JsonSerializer.Serialize(form, jsonSerializerOptions);
var back = JsonSerializer.Deserialize<Form>(json, jsonSerializerOptions);
var json2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
Another annotation is related to Newtonsoft.Json: I converted the object to Json and it was good without any particular configuration. When I tried to convert the result Json in the object, I got issues in the conversion.

Polymorphism support is released as preview versions(v7).
https://github.com/dotnet/runtime/issues/63747

Related

List of objects of derived types and JSON serializer [duplicate]

I try to migrate from Newtonsoft.Json to System.Text.Json.
I want to deserialize abstract class. Newtonsoft.Json has TypeNameHandling for this.
Is there any way to deserialize abstract class via System.Text.Json on .net core 3.0?
Is polymorphic deserialization possible in System.Text.Json?
The answer is yes and no, depending on what you mean by "possible".
There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).
Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.
However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.
The docs show an example of how to do that using a type discriminator property:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization
Let's look at an example.
Say you have a base class and a couple of derived classes:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
}
You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):
private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };
// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};
string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/
var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);
// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/
var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);
Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}
Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism:
Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?
I ended up with that solution. It's lightwight and a generic enough for me.
The type discriminator converter
public class TypeDiscriminatorConverter<T> : JsonConverter<T> where T : ITypeDiscriminator
{
private readonly IEnumerable<Type> _types;
public TypeDiscriminatorConverter()
{
var type = typeof(T);
_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty(nameof(ITypeDiscriminator.TypeDiscriminator), out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T) JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
The interface
public interface ITypeDiscriminator
{
string TypeDiscriminator { get; }
}
And the example models
public interface ISurveyStepResult : ITypeDiscriminator
{
string Id { get; set; }
}
public class BoolStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(BoolStepResult);
public bool Value { get; set; }
}
public class TextStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(TextStepResult);
public string Value { get; set; }
}
public class StarsStepResult : ISurveyStepResult
{
public string Id { get; set; }
public string TypeDiscriminator => nameof(StarsStepResult);
public int Value { get; set; }
}
And here is the test method
public void SerializeAndDeserializeTest()
{
var surveyResult = new SurveyResultModel()
{
Id = "id",
SurveyId = "surveyId",
Steps = new List<ISurveyStepResult>()
{
new BoolStepResult(){ Id = "1", Value = true},
new TextStepResult(){ Id = "2", Value = "some text"},
new StarsStepResult(){ Id = "3", Value = 5},
}
};
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = { new TypeDiscriminatorConverter<ISurveyStepResult>()},
WriteIndented = true
};
var result = JsonSerializer.Serialize(surveyResult, jsonSerializerOptions);
var back = JsonSerializer.Deserialize<SurveyResultModel>(result, jsonSerializerOptions);
var result2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
Assert.IsTrue(back.Steps.Count == 3
&& back.Steps.Any(x => x is BoolStepResult)
&& back.Steps.Any(x => x is TextStepResult)
&& back.Steps.Any(x => x is StarsStepResult)
);
Assert.AreEqual(result2, result);
}
Please try this library I wrote as an extension to System.Text.Json to offer polymorphism:
https://github.com/dahomey-technologies/Dahomey.Json
If the actual type of a reference instance differs from the declared type, the discriminator property will be automatically added to the output json:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
}
public class WeatherForecastDerived : WeatherForecast
{
public int WindSpeed { get; set; }
}
Inherited classes must be manually registered to the discriminator convention registry in order to let the framework know about the mapping between a discriminator value and a type:
JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();
DiscriminatorConventionRegistry registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterType<WeatherForecastDerived>();
string json = JsonSerializer.Serialize<WeatherForecast>(weatherForecastDerived, options);
Result:
{
"$type": "Tests.WeatherForecastDerived, Tests",
"Date": "2019-08-01T00:00:00-07:00",
"TemperatureCelsius": 25,
"Summary": "Hot",
"WindSpeed": 35
}
Polymorphic serialization of whitelisted inherited types has been implemented in .NET 7, and is available in Preview 6.
From the documentation page What’s new in System.Text.Json in .NET 7: Type Hierarchies:
System.Text.Json now supports polymorphic serialization and deserialization of user-defined type hierarchies. This can be enabled by decorating the base class of a type hierarchy with the new JsonDerivedTypeAttribute.
First, let's consider serialization. Say you have the following type hierarchy:
public abstract class BaseType { } // Properties omitted
public class DerivedType1 : BaseType { public string Derived1 { get; set; } }
public class DerivedType2 : BaseType { public int Derived2 { get; set; } }
And you have a data model that includes a value whose declared type is BaseType, e.g.
var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };
In previous versions, System.Text.Json would only serialize the properties of the declared type BaseType. Now you will be able to include the properties of DerivedType1 when serializing a value declared as BaseType by adding [JsonDerivedType(typeof(TDerivedType))] to BaseType for all derived types:
[JsonDerivedType(typeof(DerivedType1))]
[JsonDerivedType(typeof(DerivedType2))]
public abstract class BaseType { } // Properties omitted
Having whitelisted DerivedType1 in this manner, serialization of your model:
var json = JsonSerializer.Serialize(list);
Results in
[{"Derived1" : "value 1"}]
Demo fiddle #1 here.
Do note that only derived types whitelisted via attribute (or through setting JsonTypeInfo.PolymorphismOptions in runtime) can be serialized via this mechanism. If you have some other derived type which is not whitelisted, e.g.:
public class DerivedType3 : BaseType { public string Derived3 { get; set; } }
Then JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } }) will throw a System.NotSupportedException: Runtime type 'DerivedType3' is not supported by polymorphic type 'BaseType' exception. Demo fiddle #2 here.
That covers serialization. If you need to round-trip your type hierarchy, you will need to supply a type discriminator property value to use for each derived type. This may be done providing a value for JsonDerivedTypeAttribute.TypeDiscriminator for each derived type:
[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted
Now when you serialize your model
var json = JsonSerializer.Serialize(list);
System.Text.Json will add an artificial type discriminator property "$type" indicating the type that was serialized:
[{"$type" : "DerivedType1", "Derived1" : "value 1"}]
Having done so, you can now deserialize your data model like so:
var list2 = JsonSerializer.Deserialize<List<BaseType>>(json);
And the actual, concrete type(s) serialized will be preserved. Demo fiddle #3 here.
It is also possible to inform System.Text.Json of your type hierarchy in runtime via Contract Customization. You might need to do this when your type hierarchy cannot be modified, or when some derived types are in different assemblies and cannot be referenced at compile time, or you are trying to interoperate between multiple legacy serializers. The basic workflow here will be to instantiate an instance of DefaultJsonTypeInfoResolver and add a modifier which sets up the necessary PolymorphismOptions for the JsonTypeInfo for your base type.
For example, polymorphic serialization for the BaseType hierarchy can be enabled in runtime like so:
var resolver = new DefaultJsonTypeInfoResolver
{
Modifiers =
{
// Add an Action<JsonTypeInfo> modifier that sets up the polymorphism options for BaseType
static typeInfo =>
{
if (typeInfo.Type != typeof(BaseType))
return;
typeInfo.PolymorphismOptions = new()
{
DerivedTypes =
{
new JsonDerivedType(typeof(DerivedType1), "Derived1"),
new JsonDerivedType(typeof(DerivedType2), "Derived2")
}
};
},
// Add other modifiers as required.
}
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = resolver,
// Add other options as required
};
var json = JsonSerializer.Serialize(list, options);
Demo fiddle #4 here.
Notes:
The whitelisting approach is consistent with the approach of the data contract serializers, which use the KnownTypeAttribute, and XmlSerializer, which uses XmlIncludeAttribute. It is inconsistent with Json.NET, whose TypeNameHandling serializes type information for all types unless explicitly filtered via a serialization binder.
Allowing only whitelisted types to be deserialized prevents Friday the 13th: JSON Attacks type injection attacks including those detailed in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.
Integers as well as strings may be used for the type discriminator name. If you define your type hierarchy as follows:
[JsonDerivedType(typeof(DerivedType1), 1)]
[JsonDerivedType(typeof(DerivedType2), 2)]
public abstract class BaseType { } // Properties omitted
Then serializing the list above results in
[{"$type" : 1, "Derived1" : "value 1"}]
Numeric type discriminator values are not used by Newtonsoft however, so if you are interoperating with a legacy serializer you might want to avoid this.
The default type discriminator property name, "$type", is the same type discriminator name used by Json.NET. If you would prefer to use a different property name, such as the name "__type" used by DataContractJsonSerializer, apply JsonPolymorphicAttribute to the base type and set TypeDiscriminatorPropertyName like so:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]
[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted
If you are interoperating with Json.NET (or DataContractJsonSerializer), you may set the value of TypeDiscriminator equal to the type discriminator value used by the legacy serializer.
If the serializer encounters a derived type that has not been whitelisted, you can control its behavior by setting JsonPolymorphicAttribute.UnknownDerivedTypeHandling to one of the following values:
JsonUnknownDerivedTypeHandling
Value
Meaning
FailSerialization
0
An object of undeclared runtime type will fail polymorphic serialization.
FallBackToBaseType
1
An object of undeclared runtime type will fall back to the serialization contract of the base type.
FallBackToNearestAncestor
2
An object of undeclared runtime type will revert to the serialization contract of the nearest declared ancestor type. Certain interface hierarchies are not supported due to diamond ambiguity constraints.
Thats my JsonConverter for all abstract types:
private class AbstractClassConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("JsonTokenType.StartObject not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type")
throw new JsonException("Property $type not found.");
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
throw new JsonException("Value at $type is invalid.");
string assemblyQualifiedName = reader.GetString();
var type = Type.GetType(assemblyQualifiedName);
using (var output = new MemoryStream())
{
ReadObject(ref reader, output, options);
return JsonSerializer.Deserialize(output.ToArray(), type, options);
}
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
{
Encoder = options.Encoder,
Indented = options.WriteIndented
}))
{
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if(objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString());
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var valueType = value.GetType();
var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth
}))
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
jsonProperty.WriteTo(writer);
}
writer.WriteEndObject();
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
}
I really liked the answer of Demetrius, but I think you can go even further in terms of re-usability. I came up with the following solution:
The JsonConverterFactory:
/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to create <see cref="AbstractClassConverter{T}"/>
/// </summary>
public class AbstractClassConverterFactory
: JsonConverterFactory
{
/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> containing the mappings of types to their respective <see cref="JsonConverter"/>
/// </summary>
protected static Dictionary<Type, JsonConverter> Converters = new Dictionary<Type, JsonConverter>();
/// <summary>
/// Initializes a new <see cref="AbstractClassConverterFactory"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverterFactory(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert.IsAbstract && typeToConvert.IsDefined(typeof(DiscriminatorAttribute));
}
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if(!Converters.TryGetValue(typeToConvert, out JsonConverter converter))
{
Type converterType = typeof(AbstractClassConverter<>).MakeGenericType(typeToConvert);
converter = (JsonConverter)Activator.CreateInstance(converterType, this.NamingPolicy);
Converters.Add(typeToConvert, converter);
}
return converter;
}
}
The JsonConverter:
/// <summary>
/// Represents the <see cref="JsonConverter"/> used to convert to/from an abstract class
/// </summary>
/// <typeparam name="T">The type of the abstract class to convert to/from</typeparam>
public class AbstractClassConverter<T>
: JsonConverter<T>
{
/// <summary>
/// Initializes a new <see cref="AbstractClassConverter{T}"/>
/// </summary>
/// <param name="namingPolicy">The current <see cref="JsonNamingPolicy"/></param>
public AbstractClassConverter(JsonNamingPolicy namingPolicy)
{
this.NamingPolicy = namingPolicy;
DiscriminatorAttribute discriminatorAttribute = typeof(T).GetCustomAttribute<DiscriminatorAttribute>();
if (discriminatorAttribute == null)
throw new NullReferenceException($"Failed to find the required '{nameof(DiscriminatorAttribute)}'");
this.DiscriminatorProperty = typeof(T).GetProperty(discriminatorAttribute.Property, BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance);
if (this.DiscriminatorProperty == null)
throw new NullReferenceException($"Failed to find the specified discriminator property '{discriminatorAttribute.Property}' in type '{typeof(T).Name}'");
this.TypeMappings = new Dictionary<string, Type>();
foreach (Type derivedType in TypeCacheUtil.FindFilteredTypes($"nposm:json-polymorph:{typeof(T).Name}",
(t) => t.IsClass && !t.IsAbstract && t.BaseType == typeof(T)))
{
DiscriminatorValueAttribute discriminatorValueAttribute = derivedType.GetCustomAttribute<DiscriminatorValueAttribute>();
if (discriminatorValueAttribute == null)
continue;
string discriminatorValue = null;
if (discriminatorValueAttribute.Value.GetType().IsEnum)
discriminatorValue = EnumHelper.Stringify(discriminatorValueAttribute.Value, this.DiscriminatorProperty.PropertyType);
else
discriminatorValue = discriminatorValueAttribute.Value.ToString();
this.TypeMappings.Add(discriminatorValue, derivedType);
}
}
/// <summary>
/// Gets the current <see cref="JsonNamingPolicy"/>
/// </summary>
protected JsonNamingPolicy NamingPolicy { get; }
/// <summary>
/// Gets the discriminator <see cref="PropertyInfo"/> of the abstract type to convert
/// </summary>
protected PropertyInfo DiscriminatorProperty { get; }
/// <summary>
/// Gets an <see cref="Dictionary{TKey, TValue}"/> containing the mappings of the converted type's derived types
/// </summary>
protected Dictionary<string, Type> TypeMappings { get; }
/// <inheritdoc/>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Start object token type expected");
using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
{
string discriminatorPropertyName = this.NamingPolicy?.ConvertName(this.DiscriminatorProperty.Name);
if (!jsonDocument.RootElement.TryGetProperty(discriminatorPropertyName, out JsonElement discriminatorProperty))
throw new JsonException($"Failed to find the required '{this.DiscriminatorProperty.Name}' discriminator property");
string discriminatorValue = discriminatorProperty.GetString();
if (!this.TypeMappings.TryGetValue(discriminatorValue, out Type derivedType))
throw new JsonException($"Failed to find the derived type with the specified discriminator value '{discriminatorValue}'");
string json = jsonDocument.RootElement.GetRawText();
return (T)JsonSerializer.Deserialize(json, derivedType);
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
The DiscriminatorAttribute:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the property used to discriminate derived types of the marked class
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorAttribute"/>
/// </summary>
/// <param name="property">The name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/></param>
public DiscriminatorAttribute(string property)
{
this.Property = property;
}
/// <summary>
/// Gets the name of the property used to discriminate derived types of the class marked by the <see cref="DiscriminatorAttribute"/>
/// </summary>
public string Property { get; }
}
The DiscriminatorValueAttribute:
/// <summary>
/// Represents the <see cref="Attribute"/> used to indicate the discriminator value of a derived type
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DiscriminatorValueAttribute
: Attribute
{
/// <summary>
/// Initializes a new <see cref="DiscriminatorValueAttribute"/>
/// </summary>
/// <param name="value">The value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/></param>
public DiscriminatorValueAttribute(object value)
{
this.Value = value;
}
/// <summary>
/// Gets the value used to discriminate the derived type marked by the <see cref="DiscriminatorValueAttribute"/>
/// </summary>
public object Value { get; }
}
And finally, an example of how to use it on classes:
[Discriminator(nameof(Type))]
public abstract class Identity
{
public virtual IdentityType Type { get; protected set; }
}
[DiscriminatorValue(IdentityType.Person)]
public class Person
: Identity
{
}
And... Voilà!
All that is left to do is to register the factory:
this.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AbstractClassConverterFactory(options.JsonSerializerOptions.PropertyNamingPolicy));
});
Throwing this option out there: Using a source code generator to generate a JsonConverter automatically for objects with a property marked with a special attribute
You can try it with this package, but it requires .net5
https://github.com/wivuu/Wivuu.JsonPolymorphism
The generator looks at the type of the property marked with a discriminator attribute, and then looks for types inheriting from the type holding the discriminator to match up with each case of the enum
Source here: https://github.com/wivuu/Wivuu.JsonPolymorphism/blob/master/Wivuu.JsonPolymorphism/JsonConverterGenerator.cs
enum AnimalType
{
Insect,
Mammal,
Reptile,
Bird // <- This causes an easy to understand build error if it's missing a corresponding inherited type!
}
// My base type is 'Animal'
abstract partial record Animal( [JsonDiscriminator] AnimalType type, string Name );
// Animals with type = 'Insect' will automatically deserialize as `Insect`
record Insect(int NumLegs = 6, int NumEyes=4) : Animal(AnimalType.Insect, "Insectoid");
record Mammal(int NumNipples = 2) : Animal(AnimalType.Mammal, "Mammalian");
record Reptile(bool ColdBlooded = true) : Animal(AnimalType.Reptile, "Reptilian");
I want to throw in another implementation suitable for hierarchical, secure, bi-directional, generic usage.
The following caveats
It is a performance and memory "nightmare" but good enough for most scenarios (why: because you need to read ahead $type and then would need to go back on the reader).
It works only if the polymorphic base is abstract / never serialized as instance itself (why: because otherwise the regular converter cannot work on the derived classes as it goes into stack overflow).
Works under .NET 6 ... will not in 3.1.
Example
public abstract record QueryClause(); // the abstract is kind of important
public record AndClause(QueryClause[] SubClauses) : QueryClause();
public record OrClause(QueryClause[] SubClauses) : QueryClause();
// ...
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new BaseClassConverter<QueryClause>(
typeof(AndClause),
typeof(OrClause)));
// ...
Converter
public class BaseClassConverter<TBaseType> : JsonConverter<TBaseType>
where TBaseType : class
{
private readonly Type[] _types;
private const string TypeProperty = "$type";
public BaseClassConverter(params Type[] types)
{
_types = types;
}
public override bool CanConvert(Type type)
=> typeof(TBaseType) == type; // only responsible for the abstract base
public override TBaseType Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
TBaseType result;
if (JsonDocument.TryParseValue(ref reader, out var doc))
{
if (doc.RootElement.TryGetProperty(TypeProperty, out var typeProperty))
{
var typeName = typeProperty.GetString();
var type = _types.FirstOrDefault(t => t.Name == typeName) ?? throw new JsonException($"{TypeProperty} specifies an invalid type");
var rootElement = doc.RootElement.GetRawText();
result = JsonSerializer.Deserialize(rootElement, type, options) as TBaseType ?? throw new JsonException("target type could not be serialized");
}
else
{
throw new JsonException($"{TypeProperty} missing");
}
}
else
{
throw new JsonException("Failed to parse JsonDocument");
}
return result;
}
public override void Write(
Utf8JsonWriter writer,
TBaseType value,
JsonSerializerOptions options)
{
var type = value.GetType();
if (_types.Any(t => type.Name == t.Name))
{
var jsonElement = JsonSerializer.SerializeToElement(value, type, options);
var jsonObject = JsonObject.Create(jsonElement) ?? throw new JsonException();
jsonObject[TypeProperty] = type.Name;
jsonObject.WriteTo(writer, options);
}
else
{
throw new JsonException($"{type.Name} with matching base type {typeof(TBaseType).Name} is not registered.");
}
}
}
If you find something, shoot me a comment.
Some kudos to 1.
currently with new feature of .NET 7 we can do this without write handy codes to implement this.
see here: https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-5/
[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }
JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }
i hope this can help you
I changed a couple things based on ahsonkhan's answer.
Personally I like this way since the client can just give their object to the server.
However, the 'Type' property must be first in the object.
Base class and derived classes:
public interface IBaseClass
{
public DerivedType Type { get; set; }
}
public class DerivedA : IBaseClass
{
public DerivedType Type => DerivedType.DerivedA;
public string Str { get; set; }
}
public class DerivedB : IBaseClass
{
public DerivedType Type => DerivedType.DerivedB;
public bool Bool { get; set; }
}
private enum DerivedType
{
DerivedA = 0,
DerivedB = 1
}
You can create JsonConverter<IBaseClass> that reads and checks the 'Type' property while serializing. It will use that to figure out which type to deserialize.
The reader has to be copied since we read the first property as the type. And then we have to read the full object again (pass it to the Deserialize method).
public class BaseClassConverter : JsonConverter<IBaseClass>
{
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IBaseClass baseClass = default;
DerivedType type= (DerivedType)reader.GetInt32();
switch (type)
{
case DerivedType.DerivedA:
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case DerivedType.DerivedB:
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
IBaseClass value,
JsonSerializerOptions options)
{
switch(value)
{
case DerivedA derivedA:
JsonSerializer.Serialize(writer, derivedA, options);
break;
case DerivedB derivedB:
JsonSerializer.Serialize(writer, derivedB, options);
break;
default:
throw new NotSupportedException();
}
}
}
The client is now able to send objects as follows:
// DerivedA
{
"Type": 0,
"Str": "Hello world!"
}
// DerivedB
{
"Type": 1,
"Bool": false
}
EDIT:
Edited the Read method to be able to deal with the property name not being in the first order. Now it just reads through the json and stops until it finds the 'Type' property name
public override IBaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (typeReader.Read())
{
if (typeReader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string propertyName = typeReader.GetString();
if (propertyName.Equals(nameof(IBaseClass.Type)))
{
break;
}
typeReader.Skip();
}
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
IGraphOptions baseClass = default;
GraphType type = (GraphType)typeReader.GetInt32();
....
// The switch..
....
To be honest, I think the way this custom System.Text JsonConverter is set up is unneccesary complex and I prefer the Newtonsoft JsonConverter.
Basing on the accepted answer, but using KnownTypeAttribute to discover the types (often enumerating all types can lead to unwanted type load exceptions) , and adding the discriminator property in the converter instead of having the class implement it itself:
public class TypeDiscriminatorConverter<T> : JsonConverter<T>
{
private readonly IEnumerable<Type> _types;
public TypeDiscriminatorConverter()
{
var type = typeof(T);
var knownTypes = type.GetCustomAttributes(typeof(KnownTypeAttribute), false).OfType<KnownTypeAttribute>();
_types = knownTypes.Select(x => x.Type).ToArray();
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty("discriminator",
out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.FullName == typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T)JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(value)))
{
writer.WritePropertyName("discriminator");
writer.WriteStringValue(value.GetType().FullName);
foreach (var property in document.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}
which you can use like this:
[JsonConverter(typeof(JsonInheritanceConverter))]
[KnownType(typeof(DerivedA))]
[KnownType(typeof(DerivedB))]
public abstract class BaseClass
{
//..
}
Don't write like this
public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}
If you class contain baseClass property then you deserialize him like baseClass.
If you baseClass is abstract and contain baseClass property then you got Exception.
It's safer to write like this:
public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
public BaseClass derived { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
public BaseClass derived { get; set; }
}
public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}
public override bool CanConvert(Type type)
{
return typeof(BaseClass) == type;
}
public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA), options);
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB), options);
break;
case TypeDiscriminator.BaseClass:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (BaseClass)JsonSerializer.Deserialize(ref reader, typeof(BaseClass));
break;
default:
throw new NotSupportedException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}
return baseClass;
}
public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA, options);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB, options);
}
else if (value is BaseClass baseClass)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.BaseClass);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, baseClass);
}
else
{
throw new NotSupportedException();
}
writer.WriteEndObject();
}
}
But you BaseClass don't must contain property with type BaseClass or inheritor.
For interface property deserialization I've created a simple StaticTypeMapConverter
public class StaticTypeMapConverter<SourceType, TargetType> : JsonConverter<SourceType>
where SourceType : class
where TargetType : class, new()
{
public override SourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (SourceType)JsonSerializer.Deserialize(jsonObject, typeof(TargetType), options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, SourceType value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
You can use it like this:
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = {
new StaticTypeMapConverter<IMyInterface, MyImplementation>(),
new StaticTypeMapConverter<IMyInterface2, MyInterface2Class>(),
},
WriteIndented = true
};
var config = JsonSerializer.Deserialize<Config>(configContentJson, jsonSerializerOptions);
Not very elegant or efficient, but quick to code for a small number of child types:
List<Dictionary<string, object>> generics = JsonSerializer.Deserialize<List<Dictionary<string, object>>>(json);
List<InputOutputInstanceDto> result = new List<ParentType>();
foreach (Dictionary<string, object> item in generics)
{
switch(item["dataType"]) // use whatever field is in your parent/interface
{
case "Type1":
result.Add(JsonSerializer.Deserialize<Type1>(
JsonSerializer.Serialize(item)));
break
// add cases for each child type supported
default:
result.Add(JsonSerializer.Deserialize<ParentType>(
JsonSerializer.Serialize(item)));
break;
}
}
I like to share with you an issue I found using System.Text.Json. I followed the approach TypeDiscriminatorConverter that Demetrius Axenowski. It works very well.
My problems started when I added some annotations for the JSON. For example:
[JsonPropertyName("name")]
I have lost all day to understand why the code didn't work. I created some dummy code to understand where the problem was. All the source code is now on GitHub.
So, the problem was in the JsonPropertyName for the property I check in the converter. For example, this is a class
public class Radiobutton : ElementBase
{
[JsonPropertyName("type")]
public string Type => "Radiobutton";
public ElementType ElementType = ElementType.Radiobutton;
public List<string>? Choices { get; set; }
}
As you can see, I set the JsonPropertyName because I like to see type in lower case. Now, if I convert the class with this converter:
public class ElementTypeConverter<T> : JsonConverter<T> where T : IElementType
{
private readonly IEnumerable<Type> _types;
public ElementTypeConverter()
{
var type = typeof(T);
_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
}
public override T Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
using (var jsonDocument = JsonDocument.ParseValue(ref reader))
{
if (!jsonDocument.RootElement.TryGetProperty(
nameof(IElementType.Type), out var typeProperty))
{
throw new JsonException();
}
var type = _types.FirstOrDefault(x => x.Name ==
typeProperty.GetString());
if (type == null)
{
throw new JsonException();
}
var jsonObject = jsonDocument.RootElement.GetRawText();
var result = (T)JsonSerializer.Deserialize(jsonObject, type, options);
return result;
}
}
public override void Write(Utf8JsonWriter writer, T value,
JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (object)value, options);
}
}
I get the following error:
Test method SurveyExampleNetStardard21.Tests.UnitTest1.TestConversionJson_SystemTextJson_3Textbox_1radiobutton threw exception:
System.Text.Json.JsonException: The JSON value could not be converted to System.Collections.Generic.List`1[SurveyExampleNetStardard21.Interfaces.IElement]. Path: $.Elements[3] | LineNumber: 42 | BytePositionInLine: 5.
I removed the JsonPropertyName and it works fine. I tried to set
[JsonPropertyName("Type")]
(basically, the same as the variable) and it works fine. So, don't change the name. The converter is working both ways (object to Json and Json to object). This is the test code:
var jsonSerializerOptions = new JsonSerializerOptions()
{
Converters = { new ElementTypeConverter<IElement>() },
WriteIndented = true
};
var json = JsonSerializer.Serialize(form, jsonSerializerOptions);
var back = JsonSerializer.Deserialize<Form>(json, jsonSerializerOptions);
var json2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
Another annotation is related to Newtonsoft.Json: I converted the object to Json and it was good without any particular configuration. When I tried to convert the result Json in the object, I got issues in the conversion.
Polymorphism support is released as preview versions(v7).
https://github.com/dotnet/runtime/issues/63747

Custom JSON serializer for optional property with System.Text.Json

I'm trying to implement a JSON serialization mechanism which handles both null and missing JSON values, to be able to perform partial updates when needed (so that it does not touch the field in the database when the value is missing, but it clears it when the value is explicitly set to null).
I created a custom struct copied from Roslyn's Optional<T> type:
public readonly struct Optional<T>
{
public Optional(T value)
{
this.HasValue = true;
this.Value = value;
}
public bool HasValue { get; }
public T Value { get; }
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}
Now I want to be able to serialize/deserialize to/from JSON so that any missing field in JSON is preserved when roundtripping it through the Optional<T> object:
public class CustomType
{
[JsonPropertyName("foo")]
public Optional<int?> Foo { get; set; }
[JsonPropertyName("bar")]
public Optional<int?> Bar { get; set; }
[JsonPropertyName("baz")]
public Optional<int?> Baz { get; set; }
}
Then:
var options = new JsonSerializerOptions();
options.Converters.Add(new OptionalConverter());
string json = #"{""foo"":0,""bar"":null}";
CustomType parsed = JsonSerializer.Deserialize<CustomType>(json, options);
string roundtrippedJson = JsonSerializer.Serialize(parsed, options);
// json and roundtrippedJson should be equivalent
Console.WriteLine("json: " + json);
Console.WriteLine("roundtrippedJson: " + roundtrippedJson);
I started an implementation based on JsonConverterFactory, but I can't seem to find a proper way to omit the property during serialization if the optional's HasValue is false:
public class OptionalConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType) { return false; }
if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; }
return true;
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type valueType = typeToConvert.GetGenericArguments()[0];
return (JsonConverter)Activator.CreateInstance(
type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }),
bindingAttr: BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null
);
}
private class OptionalConverterInner<T> : JsonConverter<Optional<T>>
{
public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
T value = JsonSerializer.Deserialize<T>(ref reader, options);
return new Optional<T>(value);
}
public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options)
{
// Does not work (produces invalid JSON).
// Problem: the object's key has already been written in the JSON writer at this point.
if (value.HasValue)
{
JsonSerializer.Serialize(writer, value.Value, options);
}
}
}
}
Problem: this produces the following output, which is invalid:
json: {"foo":0,"bar":null}
roundtrippedJson: {"foo":0,"bar":null,"baz":}
How can I solve this?
A custom JsonConverter<T> cannot prevent the serialization of a value to which the converter applies, see [System.Text.Json] Converter-level conditional serialization #36275 for confirmation.
In .Net 5 there is an option to ignore default values which should do what you need, see How to ignore properties with System.Text.Json. This version introduces JsonIgnoreCondition.WhenWritingDefault:
public enum JsonIgnoreCondition
{
// Property is never ignored during serialization or deserialization.
Never = 0,
// Property is always ignored during serialization and deserialization.
Always = 1,
// If the value is the default, the property is ignored during serialization.
// This is applied to both reference and value-type properties and fields.
WhenWritingDefault = 2,
// If the value is null, the property is ignored during serialization.
// This is applied only to reference-type properties and fields.
WhenWritingNull = 3,
}
You will be able to apply the condition to specific properties via [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] or globally by setting JsonSerializerOptions.DefaultIgnoreCondition.
Thus in .Net 5 your class would look like:
public class CustomType
{
[JsonPropertyName("foo")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<int?> Foo { get; set; }
[JsonPropertyName("bar")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<int?> Bar { get; set; }
[JsonPropertyName("baz")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<int?> Baz { get; set; }
}
And the HasValue check should be removed from OptionalConverterInner<T>.Write():
public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value.Value, options);
Demo fiddle #1 here.
In .Net 3, as there is no conditional serialization mechanism in System.Text.Json, your only option to conditionally omit optional properties without a value is to write a custom JsonConverter<T> for all classes that contain optional properties. This is not made easy by the fact that JsonSerializer does not provide any access to its internal contract information so we need to either handcraft a converter for each and every such type, or write our own generic code via reflection.
Here is one attempt to create such generic code:
public interface IHasValue
{
bool HasValue { get; }
object GetValue();
}
public readonly struct Optional<T> : IHasValue
{
public Optional(T value)
{
this.HasValue = true;
this.Value = value;
}
public bool HasValue { get; }
public T Value { get; }
public object GetValue() => Value;
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}
public class TypeWithOptionalsConverter<T> : JsonConverter<T> where T : class, new()
{
class TypeWithOptionalsConverterContractFactory : JsonObjectContractFactory<T>
{
protected override Expression CreateSetterCastExpression(Expression e, Type t)
{
// (Optional<Nullable<T>>)(object)default(T) does not work, even though (Optional<Nullable<T>>)default(T) does work.
// To avoid the problem we need to first cast to Nullable<T>, then to Optional<Nullable<T>>
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Optional<>))
return Expression.Convert(Expression.Convert(e, t.GetGenericArguments()[0]), t);
return base.CreateSetterCastExpression(e, t);
}
}
static readonly TypeWithOptionalsConverterContractFactory contractFactory = new TypeWithOptionalsConverterContractFactory();
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var properties = contractFactory.GetProperties(typeToConvert);
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
var value = new T();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
return value;
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string propertyName = reader.GetString();
if (!properties.TryGetValue(propertyName, out var property) || property.SetValue == null)
{
reader.Skip();
}
else
{
var type = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>)
? property.PropertyType.GetGenericArguments()[0] : property.PropertyType;
var item = JsonSerializer.Deserialize(ref reader, type, options);
property.SetValue(value, item);
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var property in contractFactory.GetProperties(value.GetType()))
{
if (options.IgnoreReadOnlyProperties && property.Value.SetValue == null)
continue;
var item = property.Value.GetValue(value);
if (item is IHasValue hasValue)
{
if (!hasValue.HasValue)
continue;
writer.WritePropertyName(property.Key);
JsonSerializer.Serialize(writer, hasValue.GetValue(), options);
}
else
{
if (options.IgnoreNullValues && item == null)
continue;
writer.WritePropertyName(property.Key);
JsonSerializer.Serialize(writer, item, property.Value.PropertyType, options);
}
}
writer.WriteEndObject();
}
}
public class JsonPropertyContract<TBase>
{
internal JsonPropertyContract(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
{
this.GetValue = ExpressionExtensions.GetPropertyFunc<TBase>(property).Compile();
if (property.GetSetMethod() != null)
this.SetValue = ExpressionExtensions.SetPropertyFunc<TBase>(property, setterCastExpression).Compile();
this.PropertyType = property.PropertyType;
}
public Func<TBase, object> GetValue { get; }
public Action<TBase, object> SetValue { get; }
public Type PropertyType { get; }
}
public class JsonObjectContractFactory<TBase>
{
protected virtual Expression CreateSetterCastExpression(Expression e, Type t) => Expression.Convert(e, t);
ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>> Properties { get; } =
new ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>>();
ReadOnlyDictionary<string, JsonPropertyContract<TBase>> CreateProperties(Type type)
{
if (!typeof(TBase).IsAssignableFrom(type))
throw new ArgumentException();
var dictionary = type
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
.Where(p => p.GetIndexParameters().Length == 0 && p.GetGetMethod() != null
&& !Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)))
.ToDictionary(p => p.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name ?? p.Name,
p => new JsonPropertyContract<TBase>(p, (e, t) => CreateSetterCastExpression(e, t)),
StringComparer.OrdinalIgnoreCase);
return dictionary.ToReadOnly();
}
public IReadOnlyDictionary<string, JsonPropertyContract<TBase>> GetProperties(Type type) => Properties.GetOrAdd(type, t => CreateProperties(t));
}
public static class DictionaryExtensions
{
public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) =>
new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());
}
public static class ExpressionExtensions
{
public static Expression<Func<T, object>> GetPropertyFunc<T>(PropertyInfo property)
{
// (x) => (object)x.Property;
var arg = Expression.Parameter(typeof(T), "x");
var getter = Expression.Property(arg, property);
var cast = Expression.Convert(getter, typeof(object));
return Expression.Lambda<Func<T, object>>(cast, arg);
}
public static Expression<Action<T, object>> SetPropertyFunc<T>(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
{
//(x, y) => x.Property = (TProperty)y
var arg1 = Expression.Parameter(typeof(T), "x");
var arg2 = Expression.Parameter(typeof(object), "y");
var cast = setterCastExpression(arg2, property.PropertyType);
var setter = Expression.Call(arg1, property.GetSetMethod(), cast);
return Expression.Lambda<Action<T, object>>(setter, arg1, arg2);
}
}
Notes:
CustomType remains as shown in your question.
No attempt was made to handle the presence of a naming policy in JsonSerializerOptions.PropertyNamingPolicy. You could implement this in TypeWithOptionalsConverter<T> if necessary.
I added a non-generic interface IHasValue to enable easier access to a boxed Optional<T> during serialization.
Demo fiddle #2 here.
Alternatively, you could stick with Json.NET which supports this at the property and contact level. See:
Optionally serialize a property based on its runtime value (essentially a duplicate of your question).
how to dynamic jsonignore according to user authorize?

Custom JSON.Net serializer for generic classes [duplicate]

I have a generic type that wraps a single primitive type to give it value equality semantics
public class ValueObject<T>
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
}
It is used like:
public class CustomerId : ValueObject<Guid>
{
public CustomerId(Guid value) : base(value) { }
}
public class EmailAddress : ValueObject<string>
{
public EmailAddress(string value) : base(value) { }
}
The issue is when serializing a type like:
public class Customer
{
public CustomerId Id { get; }
public EmailAddress Email { get; }
public Customer(CustomerId id, EmailAddress email)
{
Id = id;
Email = email;
}
}
Each object the inherits from ValueObject<T> is wrapped in a Value property (as expected). For example
var customerId = new CustomerId(Guid.NewGuid());
var emailAddress = new EmailAddress("some#email.com");
var customer = new Customer(customerId, emailAddress);
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
Results in
{
"id": {
"value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
},
"email": {
"value": "some#email.com"
}
}
Is there a way to write a custom JsonConverter so the the Value property is excluded for types subclassing ValueObject<T> so that the above example would output
{
"id": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c",
"email": "some#email.com"
}
I would prefer to have a single JsonConverter that can handle all ValueObject<T> rather than having to define a separate JsonConverter for each ValueObject<T> subclass
My first attempt was
public class ValueObjectOfTConverter : JsonConverter
{
private static readonly Type ValueObjectGenericType = typeof(ValueObject<>);
private static readonly string ValuePropertyName = nameof(ValueObject<object>.Value);
public override bool CanConvert(Type objectType) =>
IsSubclassOfGenericType(objectType, ValueObjectGenericType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// converts "f5ce21a5-a0d1-4888-8d22-6f484794ac7c" => "value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
var existingJsonWrappedInValueProperty = new JObject(new JProperty(ValuePropertyName, JToken.Load(reader)));
return existingJsonWrappedInValueProperty.ToObject(objectType, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// to implement
}
private static bool IsSubclassOfGenericType(Type typeToCheck, Type openGenericType)
{
while (typeToCheck != null && typeToCheck != typeof(object))
{
var cur = typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck;
if (openGenericType == cur) return true;
typeToCheck = typeToCheck.BaseType;
}
return false;
}
}
You can do this with a custom JsonConverter similar to the ones shown in Json.Net: Serialize/Deserialize property as a value, not as an object. However, since ValueObject<T> does not have a non-generic method to get and set the Value as an object, you will need to use reflection.
Here's one approach:
class ValueConverter : JsonConverter
{
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
public override bool CanConvert(Type objectType)
{
return GetValueType(objectType) != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// You need to decide whether a null JSON token results in a null ValueObject<T> or
// an allocated ValueObject<T> with a null Value.
if (reader.SkipComments().TokenType == JsonToken.Null)
return null;
var valueType = GetValueType(objectType);
var value = serializer.Deserialize(reader, valueType);
// Here we assume that every subclass of ValueObject<T> has a constructor with a single argument, of type T.
return Activator.CreateInstance(objectType, value);
}
const string ValuePropertyName = nameof(ValueObject<object>.Value);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var valueProperty = contract.Properties.Where(p => p.UnderlyingName == ValuePropertyName).Single();
// You can simplify this to .Single() if ValueObject<T> has no other properties:
// var valueProperty = contract.Properties.Single();
serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
You could then apply the converter directly to ValueType<T> like so:
[JsonConverter(typeof(ValueConverter))]
public class ValueObject<T>
{
// Remainder unchanged
}
Or apply it in settings instead:
var settings = new JsonSerializerSettings
{
Converters = { new ValueConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, settings);
Working sample .Net fiddle #1 here.
Alternatively, you might consider adding a non-generic method to access the value as an object, e.g. like so:
public interface IHasValue
{
object GetValue(); // A method rather than a property to ensure the non-generic value is never serialized directly.
}
public class ValueObject<T> : IHasValue
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
#region IHasValue Members
object IHasValue.GetValue() => Value;
#endregion
}
With this addition, WriteJson() becomes much simpler:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, ((IHasValue)value).GetValue());
}
Working sample .Net fiddle #2 here.
Notes:
ReadJson() assumes that every subclass of Value<T> has a public constructor taking a single argument of type T.
Applying the converter directly to ValueType<T> using [JsonConverter(typeof(ValueConverter))] will have slightly better performance, since CanConvert need never get called. See Performance Tips: JsonConverters for details.
You need to decide how to handle a null JSON token. Should it result in a null ValueType<T>, or an allocated ValueType<T> with a null Value?
In the second version of ValueType<T> I implemented IHasValue.GetValue() explicitly to discourage its use in cases where an instance of ValueType<T> is used in statically typed code.
If you really only want to apply the converter to types subclassing ValueObject<T> and not ValueObject<T> itself, in GetValueType(Type objectType) add a call to .Skip(1):
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Skip(1) // Do not apply the converter to ValueObject<T> when not subclassed
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
And then apply the converter in JsonSerializerSettings.Converters rather than directly to ValueObject<T>.

.NET core 3: Order of serialization for JsonPropertyName (System.Text.Json.Serialization)

While migrating to .NET Core 3 I've switched from Newtonsoft.Json serialization to System.Text.Json.Serialization. Of all the features I want to continue using JsonPropertyName attribute.
Newtonsoft version allowed ordering of serialized attributes:
[JsonProperty(Order = 1)]
public bool Deleted { get; set; }
[JsonProperty(Order = 2)]
public DateTime DeletedDate { get; set; }
Is there a way to achieve the same in System.Text.Json.Serialization?
While this feature is not implemented in .NET Core, we can apply desired ordering by creating a custom JsonConverter. There are a few ways how that can be achievable. Below is the implementation I've came up with.
Explanation - the JsonPropertyOrderConverter handles the types having at least one property with a custom order value applied. For each of those types, it creates and caches a sorter function that converts an original object into an ExpandoObject with the properties set in a specific order. ExpandoObject maintains the order of properties, so it can be passed back to JsonSerializer for further serialization. The converter also respects JsonPropertyNameAttribute and JsonPropertyOrderAttribute attributes applied to serializing properties.
Please note that Sorter functions deal with PropertyInfo objects that can add some extra latency. If the performance is critical in your scenario, consider implementing Function<object, object> sorter based on Expression trees.
class Program
{
static void Main(string[] args)
{
var test = new Test { Bar = 1, Baz = 2, Foo = 3 };
// Add JsonPropertyOrderConverter to enable ordering
var opts = new JsonSerializerOptions();
opts.Converters.Add(new JsonPropertyOrderConverter());
var serialized = JsonSerializer.Serialize(test, opts);
// Outputs: {"Bar":1,"Baz":2,"Foo":3}
Console.WriteLine(serialized);
}
}
class Test
{
[JsonPropertyOrder(1)]
public int Foo { get; set; }
[JsonPropertyOrder(-1)]
public int Bar { get; set; }
// Default order is 0
public int Baz { get; set; }
}
/// <summary>
/// Sets a custom serialization order for a property.
/// The default value is 0.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
sealed class JsonPropertyOrderAttribute : Attribute
{
public int Order { get; }
public JsonPropertyOrderAttribute(int order)
{
Order = order;
}
}
/// <summary>
/// For Serialization only.
/// Emits properties in the specified order.
/// </summary>
class JsonPropertyOrderConverter : JsonConverter<object>
{
delegate ExpandoObject SorterFunc(object value, bool ignoreNullValues);
private static readonly ConcurrentDictionary<Type, SorterFunc> _sorters
= new ConcurrentDictionary<Type, SorterFunc>();
public override bool CanConvert(Type typeToConvert)
{
// Converter will not run if there is no custom order applied
var sorter = _sorters.GetOrAdd(typeToConvert, CreateSorter);
return sorter != null;
}
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException();
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
// Resolve the sorter.
// It must exist here (see CanConvert).
var sorter = _sorters.GetOrAdd(value.GetType(), CreateSorter);
// Convert value to an ExpandoObject
// with a certain property order
var sortedValue = sorter(value, options.IgnoreNullValues);
// Serialize the ExpandoObject
JsonSerializer.Serialize(writer, (IDictionary<string, object>)sortedValue, options);
}
private SorterFunc CreateSorter(Type type)
{
// Get type properties ordered according to JsonPropertyOrder value
var sortedProperties = type
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetCustomAttribute<JsonIgnoreAttribute>(true) == null)
.Select(x => new
{
Info = x,
Name = x.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name ?? x.Name,
Order = x.GetCustomAttribute<JsonPropertyOrderAttribute>(true)?.Order ?? 0,
IsExtensionData = x.GetCustomAttribute<JsonExtensionDataAttribute>(true) != null
})
.OrderBy(x => x.Order)
.ToList();
// If all properties have the same order,
// there is no sense in explicit sorting
if (!sortedProperties.Any(x => x.Order != 0))
{
return null;
}
// Return a function assigning property values
// to an ExpandoObject in a specified order
return new SorterFunc((src, ignoreNullValues) =>
{
IDictionary<string, object> dst = new ExpandoObject();
var isExtensionDataProcessed = false;
foreach (var prop in sortedProperties)
{
var propValue = prop.Info.GetValue(src);
if (prop.IsExtensionData)
{
if (propValue is IDictionary extensionData)
{
if (isExtensionDataProcessed)
{
throw new InvalidOperationException($"The type '{src.GetType().FullName}' cannot have more than one property that has the attribute '{typeof(JsonExtensionDataAttribute).FullName}'.");
}
foreach (DictionaryEntry entry in extensionData)
{
dst.Add((string)entry.Key, entry.Value);
}
}
isExtensionDataProcessed = true;
}
else if (!ignoreNullValues || !(propValue is null))
{
dst.Add(prop.Name, propValue);
}
}
return (ExpandoObject)dst;
});
}
}
It's supported in .Net 6 and greater using JsonPropertyOrderAttribute:
JsonPropertyOrderAttribute Class
Specifies the property order that is present in the JSON when serializing. Lower values are serialized first. If the attribute is not specified, the default value is 0.
If multiple properties have the same value, the ordering is undefined between them.
The attribute can be applied e.g. as follows:
[JsonPropertyOrder(order : 1)]
I ended up having a 2-pass approach. First pass is my normal json serializer with all converters, pocos, etc. 2nd pass is a "normalizer" to deal with whitespace/indenting/property order/etc.
There are so many corner cases trying to do this with a converter in a single pass. Properties aren't just via reflection, they can be hidden in:
Dictionaries
[JsonExtensionData] attributes
JSonElement
other converters!
It's very challenging to write a converter that deals with all of these. So I went with the 2-pass approach. The 2nd pass just operates on JsonElement and a json writer, and so avoids all the corner cases.
(we're using this in production at: https://github.com/microsoft/PowerApps-Language-Tooling/blob/master/src/PAModel/Utility/JsonNormalizer.cs )
// Write out Json in a normalized sorted order.
// Orders properties, whitespace/indenting, etc.
internal class JsonNormalizer
{
public static string Normalize(string jsonStr)
{
using (JsonDocument doc = JsonDocument.Parse(jsonStr))
{
return Normalize(doc.RootElement);
} // free up array pool rent
}
public static string Normalize(JsonElement je)
{
var ms = new MemoryStream();
JsonWriterOptions opts = new JsonWriterOptions
{
Indented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
using (var writer = new Utf8JsonWriter(ms, opts))
{
Write(je, writer);
}
var bytes = ms.ToArray();
var str = Encoding.UTF8.GetString(bytes);
return str;
}
private static void Write(JsonElement je, Utf8JsonWriter writer)
{
switch(je.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
// !!! This is where we can order the properties.
foreach (JsonProperty x in je.EnumerateObject().OrderBy(prop => prop.Name))
{
writer.WritePropertyName(x.Name);
Write(x.Value, writer);
}
writer.WriteEndObject();
break;
// When normalizing... original msapp arrays can be in any order...
case JsonValueKind.Array:
writer.WriteStartArray();
foreach(JsonElement x in je.EnumerateArray())
{
Write(x, writer);
}
writer.WriteEndArray();
break;
case JsonValueKind.Number:
writer.WriteNumberValue(je.GetDouble());
break;
case JsonValueKind.String:
// Escape the string
writer.WriteStringValue(je.GetString());
break;
case JsonValueKind.Null:
writer.WriteNullValue();
break;
case JsonValueKind.True:
writer.WriteBooleanValue(true);
break;
case JsonValueKind.False:
writer.WriteBooleanValue(false);
break;
default:
throw new NotImplementedException($"Kind: {je.ValueKind}");
}
}
}
I think the answers here all help with the 'issue'... Here's my custom solution that has been working for me.
JsonPropertyOrderAttribute spot in the #AndreyCh answer.
Adding here as well:
/// <summary>
/// Orders a property to be in a specific order when serailizing
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonPropertyOrderAttribute : JsonAttribute
{
public JsonPropertyOrderAttribute(int order)
{
Order = order;
}
public int Order { get; }
}
But this is my converter... handling the 'reads' as well has allowed me to make it a 'global' converter in my JsonSerializerOptions.
public class JsonPropertyOrderConverter : JsonConverter<object>
{
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.GetProperties().Any(x => x.GetCustomAttribute<JsonPropertyOrderAttribute>(true) != null);
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var newOptions = new JsonSerializerOptions(options);
if (newOptions.Converters.Contains(this))
{
newOptions.Converters.Remove(this);
}
return JsonSerializer.Deserialize(ref reader, typeToConvert, newOptions);
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
var orderedProperites = value.GetType().GetProperties()
.Where(x => x.GetCustomAttribute<JsonIgnoreAttribute>(true) == null)
.Select(x => new
{
Info = x,
Order = x.GetCustomAttribute<JsonPropertyOrderAttribute>(true)?.Order ?? 0
})
.OrderBy(x => x.Order)
.Select(x => x.Info);
var work = new Dictionary<string, object>();
foreach (var property in orderedProperites)
{
if (property.PropertyType.IsClass)
{
var propValue = property.GetValue(value, null);
if (propValue == null && options.IgnoreNullValues)
{
//do nothing
}
else
{
var classObj = JsonSerializer.Deserialize<object>(JsonSerializer.Serialize(propValue, options));
var jsonPropertyName = property.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name;
if (!string.IsNullOrEmpty(jsonPropertyName))
work[jsonPropertyName] = classObj;
else
work[options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name] = classObj;
}
}
else
{
var propValue = property.GetValue(value, null);
if (propValue == null && options.IgnoreNullValues)
{
//do nothing
}
else
{
var jsonPropertyName = property.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name;
if (!string.IsNullOrEmpty(jsonPropertyName))
work[jsonPropertyName] = propValue;
else
work[options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name] = propValue;
}
}
}
var newValue = JsonSerializer.Deserialize<object>(JsonSerializer.Serialize(work));
JsonSerializer.Serialize(writer, newValue, options);
}
}

How to convert only given type even if it is stored as object? [duplicate]

Suppose I have a class like this:
public class Example {
public int TypedProperty { get; set; }
public object UntypedProperty { get; set; }
}
And suppose someone comes along and writes:
var example = new Example
{
TypedProperty = 5,
UntypedProperty = Guid.NewGuid()
}
If I serialize this with JsonConvert.SerializeObject(example), I get
{
"TypedProperty": 5,
"UntypedProperty": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
Ideally, I'd like to get something like this:
{
"TypedProperty": 5,
"UntypedProperty":
{
"$type": "System.Guid,mscorlib",
"$value": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
}
But TypeNameHandling doesn't work in this scenario. How can I (de)serialize an untyped property?
If you serialize your class with TypeNameHandling.All or TypeNameHandling.Auto,
then when the UntypedProperty property would be serialized as a JSON container (either an object or array) Json.NET should correctly serialize and deserialize it by storing type information in the JSON file in a "$type" property. However, in cases where UntypedProperty is serialized as a JSON primitive (a string, number, or Boolean) this doesn't work because, as you have noted, a JSON primitive has no opportunity to include a "$type" property.
The solution is, when serializing a type with a property of type object, to serialize wrappers classes for primitive values that can encapsulate the type information, along the lines of this answer. Here is a custom JSON converter that injects such a wrapper:
public class UntypedToTypedValueConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException("This converter should only be applied directly via ItemConverterType, not added to JsonSerializer.Converters");
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var value = serializer.Deserialize(reader, objectType);
if (value is TypeWrapper)
{
return ((TypeWrapper)value).ObjectValue;
}
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (serializer.TypeNameHandling == TypeNameHandling.None)
{
Console.WriteLine("ObjectItemConverter used when serializer.TypeNameHandling == TypeNameHandling.None");
serializer.Serialize(writer, value);
}
// Handle a couple of simple primitive cases where a type wrapper is not needed
else if (value is string)
{
writer.WriteValue((string)value);
}
else if (value is bool)
{
writer.WriteValue((bool)value);
}
else
{
var contract = serializer.ContractResolver.ResolveContract(value.GetType());
if (contract is JsonPrimitiveContract)
{
var wrapper = TypeWrapper.CreateWrapper(value);
serializer.Serialize(writer, wrapper, typeof(object));
}
else
{
serializer.Serialize(writer, value);
}
}
}
}
abstract class TypeWrapper
{
protected TypeWrapper() { }
[JsonIgnore]
public abstract object ObjectValue { get; }
public static TypeWrapper CreateWrapper<T>(T value)
{
if (value == null)
return new TypeWrapper<T>();
var type = value.GetType();
if (type == typeof(T))
return new TypeWrapper<T>(value);
// Return actual type of subclass
return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
}
}
sealed class TypeWrapper<T> : TypeWrapper
{
public TypeWrapper() : base() { }
public TypeWrapper(T value)
: base()
{
this.Value = value;
}
public override object ObjectValue { get { return Value; } }
public T Value { get; set; }
}
Then apply it to your type using [JsonConverter(typeof(UntypedToTypedValueConverter))]:
public class Example
{
public int TypedProperty { get; set; }
[JsonConverter(typeof(UntypedToTypedValueConverter))]
public object UntypedProperty { get; set; }
}
If you cannot modify the Example class in any way to add this attribute (your comment The class isn't mine to change suggests as much) you could inject the converter with a custom contract resolver:
public class UntypedToTypedPropertyContractResolver : DefaultContractResolver
{
readonly UntypedToTypedValueConverter converter = new UntypedToTypedValueConverter();
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
// See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
static UntypedToTypedPropertyContractResolver instance;
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static UntypedToTypedPropertyContractResolver() { instance = new UntypedToTypedPropertyContractResolver(); }
public static UntypedToTypedPropertyContractResolver Instance { get { return instance; } }
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
foreach (var property in contract.Properties.Concat(contract.CreatorParameters))
{
if (property.PropertyType == typeof(object)
&& property.Converter == null)
{
property.Converter = property.MemberConverter = converter;
}
}
return contract;
}
}
And use it as follows:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
ContractResolver = UntypedToTypedPropertyContractResolver.Instance,
};
var json = JsonConvert.SerializeObject(example, Formatting.Indented, settings);
var example2 = JsonConvert.DeserializeObject<Example>(json, settings);
In both cases the JSON created looks like:
{
"TypedProperty": 5,
"UntypedProperty": {
"$type": "Question38777588.TypeWrapper`1[[System.Guid, mscorlib]], Tile",
"Value": "e2983c59-5ec4-41cc-b3fe-34d9d0a97f22"
}
}
Lookup SerializeWithJsonConverters.htm and ReadingWritingJSON.
Call: JsonConvert.SerializeObject(example, new ObjectConverter());
class ObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Example);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Example e = (Example)value;
writer.WriteStartObject();
writer.WritePropertyName("TypedProperty");
writer.WriteValue(e.TypedProperty);
writer.WritePropertyName("UntypedProperty");
writer.WriteStartObject();
writer.WritePropertyName("$type");
writer.WriteValue(e.UntypedProperty.GetType().FullName);
writer.WritePropertyName("$value");
writer.WriteValue(e.UntypedProperty.ToString());
writer.WriteEndObject();
writer.WriteEndObject();
}
}

Categories