Skip to content

Instantly share code, notes, and snippets.

@keith5000
Created December 15, 2019 21:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keith5000/43a51fc521b993454d503ab3af67c9f1 to your computer and use it in GitHub Desktop.
Save keith5000/43a51fc521b993454d503ab3af67c9f1 to your computer and use it in GitHub Desktop.
JSON converter to support derived types/polymorphism (ASP.NET Core 3)
/*
This gist uses a custom JsonConverter to support derived types in ASP.NET Core 3 requests and responses. By default, .NET Core
does not support derived types in JSON serialization; the properties of derived types are not serialized in responses, and in
requests there is inherently a lack of support in JSON for identifying inherited types.
More info: https://stackoverflow.com/questions/59308763/derived-types-properties-missing-in-json-response-from-asp-net-core-api
To implement this code in your project:
1. Copy the DerivedTypeJsonConverter class below into your project.
2. For each of your base types, create a class that derives from DerivedTypeJsonConverter. Follow the MyResultJsonConverter
example below.
3. Register the converters in Startup.cs.
4. In requests to the API, objects of derived types will need to include a $type property.
Example (using the StringResult type from below): { "Value":"Hi!", "$type":"StringResult" }
*/
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CODE TO COPY
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// <para>
/// A base JSON converter that allows objects accepted or returned by the API using a base type or interface to be converted to a derived type. The derived type is
/// specified in the object's JSON in a field named $typeName. The actual format of this value is up to the classes that derive from this class.
/// </para>
/// <para>
/// Create a derived converter class for each base type or interface for which derived types will be passed to or received from the API methods. Each derived converter class
/// should 1) override <see cref="DerivedTypeJsonConverter{TBase}.NameToType(string)"/>, which receives the value of the $typeName field, and should instantiate objects based
/// on the $typeName value, and 2) override <see cref="DerivedTypeJsonConverter{TBase}.TypeToName(Type)"/> which returns the $typeName value to use based on the object's type.
/// </para>
/// </summary>
/// <typeparam name="TBase"></typeparam>
/// <remarks>
/// <para>This is almost what JSON.Net's "type name handling" does. However, type naming handling emits the object's type along with its namespace and assembly.</para>
/// </remarks>
public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase>
{
#region Abstract members
/// <summary>
/// Returns the value to use for the $type property.
/// </summary>
/// <returns></returns>
protected abstract string TypeToName(Type type);
/// <summary>
/// Returns the type that corresponds to the specified $type value.
/// </summary>
/// <param name="typeName"></param>
/// <returns></returns>
protected abstract Type NameToType(string typeName);
#endregion
#region Properties
/// <summary>
/// The name of the "type" property in JSON.
/// </summary>
private const string TypePropertyName = "$type";
#endregion
#region JsonConverter implementation
public override bool CanConvert(Type objectType)
{
return typeof(TBase) == objectType;
}
public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// get the $type value by parsing the JSON string into a JsonDocument
JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader);
jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement);
string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null;
if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName}).");
// get the JSON text that was read by the JsonDocument
string json;
using (var stream = new MemoryStream())
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) {
jsonDocument.WriteTo(writer);
writer.Flush();
json = Encoding.UTF8.GetString(stream.ToArray());
}
// deserialize the JSON to the type specified by $type
try {
return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options);
}
catch (Exception ex) {
throw new InvalidOperationException("Invalid JSON in request.", ex);
}
}
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
{
// create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it
ExpandoObject expando = ToExpandoObject(value);
expando.TryAdd(TypePropertyName, TypeToName(value.GetType()));
// serialize the expando
JsonSerializer.Serialize(writer, expando, options);
}
#endregion
#region Private methods
/// <summary>
/// Returns an <see cref="ExpandoObject"/> whose values are copied from the specified object's public properties.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private static ExpandoObject ToExpandoObject(object obj)
{
var expando = new ExpandoObject();
if (obj != null) {
// copy all public properties
foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) {
expando.TryAdd(property.Name, property.GetValue(obj));
}
}
return expando;
}
#endregion
}
using System;
// Sample base type
public interface IMyResult { }
// Sample derived type
public class StringResult : IMyResult
{
public string Value { get; set; }
}
// Example of how to implement DerivedTypeJsonConverter
public class MyResultJsonConverter : DerivedTypeJsonConverter<IMyResult>
{
protected override Type NameToType(string typeName)
{
return typeName switch
{
nameof(StringResult) => typeof(StringResult),
// TODO: Create a case for each derived type
_ => throw new InvalidRequestException($"Unsupported type string \"{typeName}\".")
};
}
protected override string TypeToName(Type type)
{
if (type == typeof(StringResult)) return nameof(StringResult);
// TODO: Create a condition for each derived type
throw new ArgumentException($"Type {type.FullName} not supported.", nameof(type));
}
}
/*
In ConfigureServices, modify the call to AddControllers to add your converters:
*/
services.AddControllers()
.AddJsonOptions(options => {
options.JsonSerializerOptions.Converters.Add(new MyResultJsonConverter());
// TODO: Add each converter
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment