In System.Text.Json is it possible to specify custom indentation rules? - c#

Edit: I made an issue at the .Net runtime repo yesterday which was closed to by "layomia" with this message: "Adding extension points like this comes with a performance cost at the lower-level reader and writer and does not present a good balance between perf and functionality/benefit. Providing such configuration is not on the System.Text.Json roadmap."
When setting JsonSerializerOptions.WriteIndented = true indentation looks like this when writing json...
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [
304,
16,
16,
16
],
"SCREEN_BOUNDS": [
485,
159,
64,
64
]
}
}
}
Is there a way to change the automatic indentation to something like this...
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES":
{
"TILE_1":
{
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16,16],
"SCREEN_BOUNDS": [485, 159, 64, 64]
}
}
}

Update for .NET 6
While custom indentation rules are not supported by System.Text.Json, as of .NET 6 and later it is possible to disable indentation when serializing a particular member or type. By using Utf8JsonWriter.WriteRawValue(), you can create a custom JsonConverter that generates a default serialization for your value without indentation to a utf8 byte buffer, then writes the buffer to the incoming Utf8JsonWriter as-is.
First define the following converters:
public class NoIndentationConverter : NoIndentationConverter<object>
{
public override bool CanConvert(Type typeToConvert) => true;
}
public class NoIndentationConverter<T> : DefaultConverterFactory<T>
{
protected override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions)
{
// TODO: investigate https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.arraypoolbufferwriter-1
var bufferWriter = new ArrayBufferWriter<byte>();
using (var innerWriter = new Utf8JsonWriter(bufferWriter))
JsonSerializer.Serialize(innerWriter, value, modifiedOptions);
writer.WriteRawValue(bufferWriter.WrittenSpan, skipInputValidation : true);
}
protected override JsonSerializerOptions ModifyOptions(JsonSerializerOptions options) { (options = base.ModifyOptions(options)).WriteIndented = false; return options; }
}
public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
// Adapted from this answer https://stackoverflow.com/a/65430421/3744182
// To https://stackoverflow.com/questions/65430420/how-to-use-default-serialization-in-a-custom-system-text-json-jsonconverter
class DefaultConverter : JsonConverter<T>
{
readonly JsonSerializerOptions modifiedOptions;
readonly DefaultConverterFactory<T> factory;
public DefaultConverter(JsonSerializerOptions modifiedOptions, DefaultConverterFactory<T> factory) => (this.modifiedOptions, this.factory) = (modifiedOptions, factory);
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions);
public override bool CanConvert(Type typeToConvert) => typeof(T).IsAssignableFrom(typeToConvert);
}
protected virtual JsonSerializerOptions ModifyOptions(JsonSerializerOptions options)
=> options.CopyAndRemoveConverter(this.GetType());
protected virtual T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
=> (T?)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions)
=> JsonSerializer.Serialize(writer, value, modifiedOptions);
public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;
public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new DefaultConverter(ModifyOptions(options), this);
}
public static class JsonSerializerExtensions
{
public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
{
var copy = new JsonSerializerOptions(options);
for (var i = copy.Converters.Count - 1; i >= 0; i--)
if (copy.Converters[i].GetType() == converterType)
copy.Converters.RemoveAt(i);
return copy;
}
}
And now you can either apply NoIndentationConverter directly to your model (demo #1 here):
public partial class Tile1
{
[JsonPropertyName("NAME")]
public string Name { get; set; }
[JsonPropertyName("TEXTURE_BOUNDS")]
[JsonConverter(typeof(NoIndentationConverter))]
public List<long> TextureBounds { get; set; }
[JsonPropertyName("SCREEN_BOUNDS")]
[JsonConverter(typeof(NoIndentationConverter))]
public List<long> ScreenBounds { get; set; }
}
Or disable indentation for all List<long> values by adding NoIndentationConverter<List<long>> to JsonSerializerOptions.Converters as follows (demo #2 here):
var options = new JsonSerializerOptions
{
Converters = { new NoIndentationConverter<List<long>>() },
WriteIndented = true,
};
Both approaches result in your model being serialized as follows:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304,16,16,16],
"SCREEN_BOUNDS": [485,159,64,64]
}
}
}
Notes:
If your arrays are very large, the temporary ArrayBufferWriter<byte> may consume substantial memory. You might look into using ArrayPoolBufferWriter<T> instead.
This approach does not work for a value that already has a custom JsonConverter applied. But you could rewrite that converter to use the same approach above.
You cannot disable indentation for a type by applying [JsonConverter(typeof(NoIndentationConverter))] directly to the type. Once a converter has been applied to a type, it is impossible to generate a "default" serialization using System.Text.Json. For details see this answer to How to use default serialization in a custom System.Text.Json JsonConverter?.
Original Answer
This is not possible currently with System.Text.Json (as of .NET 5). Let's consider the possibilities:
JsonSerializerOptions has no method to control indentation other than the Boolean property WriteIndented:
Gets or sets a value that defines whether JSON should use pretty printing.
Utf8JsonWriter has no method to modify or control indentation, as Options is a get-only struct-valued property.
In .Net Core 3.1, if I create a custom JsonConverter<T> for your TEXTURE_BOUNDS and SCREEN_BOUNDS lists and attempt set options.WriteIndented = false; during serialization, a System.InvalidOperationException: Serializer options cannot be changed once serialization or deserialization has occurred exception will be thrown.
Specifically, if I create the following converter:
class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
var old = options.WriteIndented;
try
{
options.WriteIndented = false;
JsonSerializer.Serialize(writer, new CollectionSurrogate<TCollection, TItem>(value), options);
}
finally
{
options.WriteIndented = old;
}
}
}
public class CollectionSurrogate<TCollection, TItem> : ICollection<TItem> where TCollection : ICollection<TItem>, new()
{
public TCollection BaseCollection { get; }
public CollectionSurrogate() { this.BaseCollection = new TCollection(); }
public CollectionSurrogate(TCollection baseCollection) { this.BaseCollection = baseCollection ?? throw new ArgumentNullException(); }
public void Add(TItem item) => BaseCollection.Add(item);
public void Clear() => BaseCollection.Clear();
public bool Contains(TItem item) => BaseCollection.Contains(item);
public void CopyTo(TItem[] array, int arrayIndex) => BaseCollection.CopyTo(array, arrayIndex);
public int Count => BaseCollection.Count;
public bool IsReadOnly => BaseCollection.IsReadOnly;
public bool Remove(TItem item) => BaseCollection.Remove(item);
public IEnumerator<TItem> GetEnumerator() => BaseCollection.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable)BaseCollection).GetEnumerator();
}
And the following data model:
public partial class Root
{
[JsonPropertyName("TILESET")]
public string Tileset { get; set; }
[JsonPropertyName("TILES")]
public Tiles Tiles { get; set; }
}
public partial class Tiles
{
[JsonPropertyName("TILE_1")]
public Tile1 Tile1 { get; set; }
}
public partial class Tile1
{
[JsonPropertyName("NAME")]
public string Name { get; set; }
[JsonPropertyName("TEXTURE_BOUNDS")]
[JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
public List<long> TextureBounds { get; set; }
[JsonPropertyName("SCREEN_BOUNDS")]
[JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
public List<long> ScreenBounds { get; set; }
}
Then serializing Root throws the following exception:
Failed with unhandled exception:
System.InvalidOperationException: Serializer options cannot be changed once serialization or deserialization has occurred.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable()
at System.Text.Json.JsonSerializerOptions.set_WriteIndented(Boolean value)
at CollectionFormattingConverter`2.Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
at System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer)
at System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer)
at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteCore(Utf8JsonWriter writer, Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
Demo fiddle #1 here.
In .Net Core 3.1, if I create a custom JsonConverter<T> that creates a pre-formatted JsonDocument and then writes that out, the document will be reformatted as it is written.
I.e. if I create the following converter:
class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
var copy = options.Clone();
copy.WriteIndented = false;
using var doc = JsonExtensions.JsonDocumentFromObject(new CollectionSurrogate<TCollection, TItem>(value), copy);
Debug.WriteLine("Preformatted JsonDocument: {0}", doc.RootElement);
doc.WriteTo(writer);
}
}
public static partial class JsonExtensions
{
public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
{
if (options == null)
return new JsonSerializerOptions();
//In .Net 5 a copy constructor will be introduced for JsonSerializerOptions. Use the following in that version.
//return new JsonSerializerOptions(options);
//In the meantime copy manually.
var clone = new JsonSerializerOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
DefaultBufferSize = options.DefaultBufferSize,
DictionaryKeyPolicy = options.DictionaryKeyPolicy,
Encoder = options.Encoder,
IgnoreNullValues = options.IgnoreNullValues,
IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
MaxDepth = options.MaxDepth,
PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
PropertyNamingPolicy = options.PropertyNamingPolicy,
ReadCommentHandling= options.ReadCommentHandling,
WriteIndented = options.WriteIndented,
};
foreach (var converter in options.Converters)
clone.Converters.Add(converter);
return clone;
}
// Copied from this answer https://stackoverflow.com/a/62998253/3744182
// To https://stackoverflow.com/questions/62996999/convert-object-to-system-text-json-jsonelement
// By https://stackoverflow.com/users/3744182/dbc
public static JsonDocument JsonDocumentFromObject<TValue>(TValue value, JsonSerializerOptions options = default)
=> JsonDocumentFromObject(value, typeof(TValue), options);
public static JsonDocument JsonDocumentFromObject(object value, Type type, JsonSerializerOptions options = default)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options);
return JsonDocument.Parse(bytes);
}
}
Fully indented JSON is generated despite the fact that the intermediate JsonDocument doc was serialized without indentation:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [
304,
16,
16,
16
],
"SCREEN_BOUNDS": [
485,
159,
64,
64
]
}
}
}
Demo fiddle #2 here.
And finally, in .Net Core 3.1, if I create a custom JsonConverter<T> that clones the incoming JsonSerializerOptions, modifies WriteIndented on the copy, then recursively serializes using the copied settings -- the modified value for WriteIndented is ignored.
Demo fiddle #3 here.
Apparently the JsonConverter architecture is going to be extensively enhanced in .Net 5 so you might re-test this option when it is released.
You might want to open an issue requesting this functionality, as there are multiple popular questions about how to do this with Json.NET (where it can be done with a converter):
How to apply indenting serialization only to some properties?
Newtonsoft inline formatting for subelement while serializing
Creating JSON without array indentation

Faced with the same problem. I need to write arrays in one row for json simplicity.
Latest version is here: https://github.com/micro-elements/MicroElements.Metadata/blob/master/src/MicroElements.Metadata.SystemTextJson/SystemTextJson/Utf8JsonWriterCopier.cs
Solution:
I Use reflection to create clone of Utf8JsonWriter with desired options (see class Utf8JsonWriterCopier.cs)
To check that API was not changed Clone calls Utf8JsonWriterCopier.AssertReflectionStateIsValid, also you can use it in your tests
Usage:
Create NotIndented copy of Utf8JsonWriter
Write array
Copy internal state back to original writer
Sample:
if (Options.WriteArraysInOneRow && propertyType.IsArray && writer.Options.Indented)
{
// Creates NotIndented writer
Utf8JsonWriter writerCopy = writer.CloneNotIndented();
// PropertyValue
JsonSerializer.Serialize(writerCopy, propertyValue.ValueUntyped, propertyType, options);
// Needs to copy internal state back to writer
writerCopy.CopyStateTo(writer);
}
Utf8JsonWriterCopier.cs
/// <summary>
/// Helps to copy <see cref="Utf8JsonWriter"/> with other <see cref="JsonWriterOptions"/>.
/// This is not possible with public API so Reflection is used to copy writer internals.
/// See also: https://stackoverflow.com/questions/63376873/in-system-text-json-is-it-possible-to-specify-custom-indentation-rules.
/// Usage:
/// <code>
/// if (Options.WriteArraysInOneRow and propertyType.IsArray and writer.Options.Indented)
/// {
/// // Create NotIndented writer
/// Utf8JsonWriter writerCopy = writer.CloneNotIndented();
///
/// // Write array
/// JsonSerializer.Serialize(writerCopy, array, options);
///
/// // Copy internal state back to writer
/// writerCopy.CopyStateTo(writer);
/// }
/// </code>
/// </summary>
public static class Utf8JsonWriterCopier
{
private class Utf8JsonWriterReflection
{
private IReadOnlyCollection<string> FieldsToCopyNames { get; } = new[] { "_arrayBufferWriter", "_memory", "_inObject", "_tokenType", "_bitStack", "_currentDepth" };
private IReadOnlyCollection<string> PropertiesToCopyNames { get; } = new[] { "BytesPending", "BytesCommitted" };
private FieldInfo[] Fields { get; }
private PropertyInfo[] Properties { get; }
internal FieldInfo OutputField { get; }
internal FieldInfo StreamField { get; }
internal FieldInfo[] FieldsToCopy { get; }
internal PropertyInfo[] PropertiesToCopy { get; }
public Utf8JsonWriterReflection()
{
Fields = typeof(Utf8JsonWriter).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
Properties = typeof(Utf8JsonWriter).GetProperties(BindingFlags.Instance | BindingFlags.Public);
OutputField = Fields.FirstOrDefault(info => info.Name == "_output")!;
StreamField = Fields.FirstOrDefault(info => info.Name == "_stream")!;
FieldsToCopy = FieldsToCopyNames
.Select(name => Fields.FirstOrDefault(info => info.Name == name))
.Where(info => info != null)
.ToArray();
PropertiesToCopy = PropertiesToCopyNames
.Select(name => Properties.FirstOrDefault(info => info.Name == name))
.Where(info => info != null)
.ToArray();
}
public void AssertStateIsValid()
{
if (OutputField == null)
throw new ArgumentException("Field _output is not found. API Changed!");
if (StreamField == null)
throw new ArgumentException("Field _stream is not found. API Changed!");
if (FieldsToCopy.Length != FieldsToCopyNames.Count)
throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
if (PropertiesToCopy.Length != PropertiesToCopyNames.Count)
throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
}
}
private static readonly Utf8JsonWriterReflection _reflectionCache = new Utf8JsonWriterReflection();
/// <summary>
/// Checks that reflection API is valid.
/// </summary>
public static void AssertReflectionStateIsValid()
{
_reflectionCache.AssertStateIsValid();
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> with new <see cref="JsonWriterOptions"/>.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <param name="newOptions">Options to use in new writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/> with new options.</returns>
public static Utf8JsonWriter Clone(this Utf8JsonWriter writer, JsonWriterOptions newOptions)
{
AssertReflectionStateIsValid();
Utf8JsonWriter writerCopy;
// Get internal output to use in new writer
IBufferWriter<byte>? output = (IBufferWriter<byte>?)_reflectionCache.OutputField.GetValue(writer);
if (output != null)
{
// Create copy
writerCopy = new Utf8JsonWriter(output, newOptions);
}
else
{
// Get internal stream to use in new writer
Stream? stream = (Stream?)_reflectionCache.StreamField.GetValue(writer);
// Create copy
writerCopy = new Utf8JsonWriter(stream, newOptions);
}
// Copy internal state
writer.CopyStateTo(writerCopy);
return writerCopy;
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to false.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
public static Utf8JsonWriter CloneNotIndented(this Utf8JsonWriter writer)
{
JsonWriterOptions newOptions = writer.Options;
newOptions.Indented = false;
return Clone(writer, newOptions);
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to true.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
public static Utf8JsonWriter CloneIndented(this Utf8JsonWriter writer)
{
JsonWriterOptions newOptions = writer.Options;
newOptions.Indented = true;
return Clone(writer, newOptions);
}
/// <summary>
/// Copies internal state of one writer to another.
/// </summary>
/// <param name="sourceWriter">Source writer.</param>
/// <param name="targetWriter">Target writer.</param>
public static void CopyStateTo(this Utf8JsonWriter sourceWriter, Utf8JsonWriter targetWriter)
{
foreach (var fieldInfo in _reflectionCache.FieldsToCopy)
{
fieldInfo.SetValue(targetWriter, fieldInfo.GetValue(sourceWriter));
}
foreach (var propertyInfo in _reflectionCache.PropertiesToCopy)
{
propertyInfo.SetValue(targetWriter, propertyInfo.GetValue(sourceWriter));
}
}
/// <summary>
/// Clones <see cref="JsonSerializerOptions"/>.
/// </summary>
/// <param name="options">Source options.</param>
/// <returns>New instance of <see cref="JsonSerializerOptions"/>.</returns>
public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
{
JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
AllowTrailingCommas = options.AllowTrailingCommas,
WriteIndented = options.WriteIndented,
PropertyNamingPolicy = options.PropertyNamingPolicy,
DefaultBufferSize = options.DefaultBufferSize,
DictionaryKeyPolicy = options.DictionaryKeyPolicy,
Encoder = options.Encoder,
IgnoreNullValues = options.IgnoreNullValues,
IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
MaxDepth = options.MaxDepth,
PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
ReadCommentHandling = options.ReadCommentHandling,
};
foreach (JsonConverter jsonConverter in options.Converters)
{
serializerOptions.Converters.Add(jsonConverter);
}
return serializerOptions;
}
}

I use a custom converter to collapse Arrays and Below is an example for a hash set.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace MyProject.Core.Converters
{
public class HashSetConverter : JsonConverter<HashSet<string>?>
{
public override HashSet<string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, HashSet<string>? value, JsonSerializerOptions options)
{
if (value is not null)
{
writer.WriteRawValue($"[\"{string.Join("\", \"", value)}\"]");
}
}
}
public class ArrayConverter : JsonConverter<int[]?>
{
public override int[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, int[]? value, JsonSerializerOptions options)
{
if (value is not null)
{
writer.WriteRawValue($"[{string.Join(", ", value)}]");
}
}
}
}
Then I setup my Serializer
var serializerOptions = new JsonSerializerOptions()
{
Converters = { new HashSetConverter(), new ArrayConverter() },
WriteIndented = true,
};
var json = JsonSerializer.Serialize(
new
{
Name = "auto_tile_18",
TEXTURE_BOUNDS = new int[]
{
304,
16,
16,
16,
},
},
serializerOptions);
Resulting Json
{
"Name": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16, 16]
}

If you don't mind using an open source package or source file, take a look at FracturedJson ( nuget ), ( project home ). I wrote it specifically to create output that is easy to read without wasting tons of vertical space.
Here's what the output looks like for the original poster's data, using default options:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16, 16],
"SCREEN_BOUNDS": [485, 159, 64, 64]
}
}
}

Related

C# Json array formatting [duplicate]

NB: I am using System.Text.Json not JSON.NET for which there is similar question that has been answered. I need the same answer but for System.Text.Json. I would like to write my class to Json and ensure that it is human readable. To do this I have set the indent to true in options. However, the class contains a List<double> property which I don't want to indent as it makes the file very long.
So I have this:
public class ThingToSerialize
{
public string Name {get;set;}
//other properties here
public List<double> LargeList {get;set;}
};
var thing = new ThingToSerialize {Name = "Example", LargeList = new List<double>{0,0,0}};
var options = new JsonSerializerOptions
{
WriteIndented = true
};
options.Converters.Add(new DontIndentArraysConverter());
var s = JsonSerializer.Serialize(thing, options);
and I want it to serialize like this:
{
"Name": "Example",
"LargeList ": [0,0,0]
}
Not this (or something along these lines):
{
"Name": "Example",
"LargeList ": [
0,
0,
0
]
}
I have written a JsonConverter to achieve this:
public class DontIndentArraysConverter : JsonConverter<List<double>>
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<double>);
}
public override List<double> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<List<double>>(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, List<double> value, JsonSerializerOptions options)
{
var s = JsonSerializer.Serialize(value);
writer.WriteStringValue(s);
}
}
However this writes the array as a string which I don't really want. Whats the best approach for this?
i.e. you get "[1,2,3]" instead of [1,2,3]
Secondly, the writer object that is passed into the Write function has an Options property but this cannot be changed. So if I write the array out manually using the writer object, it is indented.
Thanks for your idea, I wrote a wrapper based on the Converter hint
The idea is write a temp value during conversion, put correct array to a temp dict and replace them later
It's a bit late for you but maybe it can help other guys
public class CustomSerializer : IDisposable
{
private readonly Dictionary<string, string> _replacement = new Dictionary<string, string>();
public string Serialize<T>(T obj)
{
var converterForListInt = new DontIndentArraysConverterForListInt(_replacement);
var options = new JsonSerializerOptions
{
IgnoreNullValues = true,
WriteIndented = true
};
options.Converters.Add(converterForListInt);
var json = JsonSerializer.Serialize(obj, options);
foreach (var (k, v) in _replacement)
json = json.Replace(k, v);
return json;
}
public void Dispose()
{
_replacement.Clear();
}
public class DontIndentArraysConverterForListInt : JsonConverter<List<int>>
{
private readonly Dictionary<string, string> _replacement;
public DontIndentArraysConverterForListInt(Dictionary<string, string> replacement)
{
_replacement = replacement;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<int>);
}
public override List<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<List<int>>(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, List<int> value, JsonSerializerOptions options)
{
if (value.Count > 0)
{
var key = $"TMP_{Guid.NewGuid().ToString()}";
var sb = new StringBuilder();
sb.Append('[');
foreach (var i in value)
{
sb.Append(i);
sb.Append(',');
}
sb.Remove(sb.Length - 1, 1); // trim last ,
sb.Append(']');
_replacement.Add($"\"{key}\"", sb.ToString());
//
writer.WriteStringValue(key);
}
else
{
// normal
writer.WriteStartArray();
writer.WriteEndArray();
}
}
}
}
Encountered this old question while trying to do the same thing for a List<string>. With .NET 6 (or at least package System.Text.Json v6.0.0.0) and later, this is now possible directly with System.Text.Json and the Utf8JsonWriter method WriteRawValue.
class ListDoubleSingleLineConverter : JsonConverter<List<double>>
{
//Read override omitted
public override void Write(Utf8JsonWriter writer, List<double> value, JsonSerializerOptions options)
{
writer.WriteRawValue(
String.Concat(
"[ ",
// Depending on your values, you might want to use LINQ .Select() and String.Format() to control the output
String.Join(", ", value),
" ]"));
}
}
Of note from the documentation:
When using this method, the input content will be written to the writer destination as-is, unless validation fails (when it is enabled).
(For my List<string> case, this meant transforming value with value.Select(v => String.Concat("\"", v, "\"")) before String.Join, as otherwise the strings were emitted unquoted; when using WriteRawValue you assume responsibility for the supplied argument being a well-formed JSON fragment.)
Tested with:
JsonSerializerOptions options = new JsonSerializerOptions()
{
WriteIndented = true,
Converters = { new ListDoubleSingleLineConverter() },
};
var test = new
{
Name = "Example",
LargeList = new List<double>() { 1.0, 1.1, 1.2, 1.3 },
OtherProperty = 27.0,
};
Console.WriteLine(JsonSerializer.Serialize(test, options));
Output:
{
"Name": "Example",
"LargeList": [ 1, 1.1, 1.2, 1.3 ],
"OtherProperty": 27
}

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

How to deserialize part of json using System.Text.Json in .net core 3.0?

I have a json from here https://api.nasa.gov/insight_weather/?api_key=DEMO_KEY&feedtype=json&ver=1.0 which looks like:
{
"782": {
"First_UTC": "2021-02-06T17:08:11Z",
"Last_UTC": "2021-02-07T17:47:46Z",
"Month_ordinal": 12,
"Northern_season": "late winter",
"PRE": {
"av": 721.77,
"ct": 113450,
"mn": 698.8193,
"mx": 742.2686
},
"Season": "winter",
"Southern_season": "late summer",
"WD": {
"most_common": null
}
},
"783": {
"First_UTC": "2021-02-07T17:47:46Z",
"Last_UTC": "2021-02-08T18:27:22Z",
"Month_ordinal": 12,
"Northern_season": "late winter",
"PRE": {
"av": 722.186,
"ct": 107270,
"mn": 698.7664,
"mx": 743.1983
},
"Season": "winter",
"Southern_season": "late summer",
"WD": {
"most_common": null
}
},
"sol_keys": [ "782", "783" ],
"validity_checks": { /* Some complex object */ }
}
I need only part of this information so I have created the following classes:
public class MarsWheather {
[JsonPropertyName("First_UTC")]
public DateTime FirstUTC { get; set; }
[JsonPropertyName("Last_UTC")]
public DateTime LastUTC { get; set; }
[JsonPropertyName("Season")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Season MarsSeason { get; set; }
[JsonPropertyName("PRE")]
public DataDescription AtmosphericPressure { get; set; }
}
public enum Season {
winter,
spring,
summer,
autumn
}
public class DataDescription{
[JsonPropertyName("av")]
public double Average { get; set; }
[JsonPropertyName("ct")]
public double TotalCount { get; set; }
[JsonPropertyName("mn")]
public double Minimum { get; set; }
[JsonPropertyName("mx")]
public double Maximum { get; set; }
}
The problem is that the JSON root object from NASA contains properties "validity_checks" and "sol_keys" that I don't need and want to skip. In Newton.Json I've used JObject.Parse to do this, but in System.Text.Json I want to use
JsonSerializer.DeserializeAsync<Dictionary<string, MarsWheather>>(stream, new JsonSerializerOptions { IgnoreNullValues = true });
Unfortunately, when I do I get an exception:
System.Text.Json.JsonException: The JSON value could not be converted to MarsWheather. Path: $.sol_keys | LineNumber: 120 | BytePositionInLine: 15.
Demo fiddle here.
Is it possible?
Your JSON root object consists of certain fixed keys ("sol_keys" and "validity_checks") whose values each have some fixed schema, and any number of variable keys (the "782" numeric keys) whose values all share a common schema that differs from the schemas of the fixed key values:
{
"782": {
// Properties corresponding to your MarsWheather object
},
"783": {
// Properties corresponding to your MarsWheather object
},
// Other variable numeric key/value pairs corresponding to KeyValuePair<string, MarsWheather>
"sol_keys": [
// Some array values you don't care about
],
"validity_checks": {
// Some object you don't care about
}
}
You would like to deserialize just the variable keys, but when you try to deserialize to a Dictionary<string, MarsWheather> you get an exception because the serializer tries to deserialize a fixed key value as if it were variable key value -- but since the fixed key has an array value while the variable keys have object values, an exception gets thrown. How can System.Text.Json be told to skip the known, fixed keys rather than trying to deserialize them?
If you want to deserialize just the variable keys and skip the fixed, known keys, you will need to create a custom JsonConverter. The easiest way to do that would be to first create some root object for your dictionary:
[JsonConverter(typeof(MarsWheatherRootObjectConverter))]
public class MarsWheatherRootObject
{
public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
}
And then define the following converter for it as follows:
public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
{
static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new Dictionary<string, ReadFixedKeyMethod>(StringComparer.OrdinalIgnoreCase)
{
{ "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
{ "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
};
protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => Enumerable.Empty<KeyValuePair<string, WriteFixedKeyMethod>>();
}
public abstract class FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> : JsonConverter<TObject>
where TDictionary : class, IDictionary<string, TValue>, new()
where TObject : new()
{
protected delegate void ReadFixedKeyMethod(ref Utf8JsonReader reader, TObject obj, string name, JsonSerializerOptions options);
protected delegate void WriteFixedKeyMethod(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options);
protected abstract TDictionary GetDictionary(TObject obj);
protected abstract void SetDictionary(TObject obj, TDictionary dictionary);
protected abstract bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method);
protected abstract IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options);
public override TObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return (typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) == null)
? throw new JsonException(string.Format("Unepected token {0}", reader.TokenType))
: default(TObject);
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
var obj = new TObject();
var dictionary = GetDictionary(obj);
var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
var name = reader.GetString();
reader.ReadAndAssert();
if (TryGetFixedKeyReadMethod(name, options, out var method))
{
method(ref reader, obj, name, options);
}
else
{
if (dictionary == null)
SetDictionary(obj, dictionary = new TDictionary());
dictionary.Add(name, valueConverter.ReadOrDeserialize(ref reader, typeof(TValue), options));
}
}
else if (reader.TokenType == JsonTokenType.EndObject)
{
return obj;
}
else
{
throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
}
}
throw new JsonException(); // Truncated file
}
public override void Write(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var dictionary = GetDictionary(value);
if (dictionary != null)
{
var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
foreach (var pair in dictionary)
{
// TODO: handle DictionaryKeyPolicy
writer.WritePropertyName(pair.Key);
valueConverter.WriteOrSerialize(writer, pair.Value, typeof(TValue), options);
}
}
foreach (var pair in GetFixedKeyWriteMethods(options))
{
writer.WritePropertyName(pair.Key);
pair.Value(writer, value, options);
}
writer.WriteEndObject();
}
}
public static partial class JsonExtensions
{
public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, Type type, JsonSerializerOptions options)
{
if (converter != null)
converter.Write(writer, value, options);
else
JsonSerializer.Serialize(writer, value, type, options);
}
public static T ReadOrDeserialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> converter != null ? converter.Read(ref reader, typeToConvert, options) : (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);
public static void ReadAndAssert(this ref Utf8JsonReader reader)
{
if (!reader.Read())
throw new JsonException();
}
}
And now you will be able to deserialize to MarsWheatherRootObject as follows:
var root = await System.Text.Json.JsonSerializer.DeserializeAsync<MarsWheatherRootObject>(
stream,
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
Demo fiddle #1 here.
Notes:
FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> provides a general framework for serializing and deserializing objects with fixed and variable properties. If later you decide to deserialize e.g. "sol_keys", you could modify MarsWheatherRootObject as follows:
[JsonConverter(typeof(MarsWheatherRootObjectConverter))]
public class MarsWheatherRootObject
{
public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
public List<string> SolKeys { get; set; } = new List<string>();
}
And the converter as follows:
public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
{
static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new(StringComparer.OrdinalIgnoreCase)
{
{ "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) =>
{
obj.SolKeys = JsonSerializer.Deserialize<List<string>>(ref reader, options);
}
},
{ "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
};
static readonly Dictionary<string, WriteFixedKeyMethod> FixedKeyWriteMethods = new Dictionary<string, WriteFixedKeyMethod>()
{
{ "sol_keys", (w, v, o) =>
{
JsonSerializer.Serialize(w, v.SolKeys, o);
}
},
};
protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => FixedKeyWriteMethods;
}
Demo fiddle #2 here.

System.Text.Json Serialize null strings into empty strings globally

While migrating code from newtonsoft json to system.text.json
I need all nullable strings to render as empty string.
I wrote following converter but all null string values are still rendered as null.
And for null string values, Write method is not called. Break point is never hit.
public class EmptyStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Convert.ToString(reader.GetString(), CultureInfo.CurrentCulture);
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (writer == null)
throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue(value ?? "");
}
}
Startup code
services.AddControllers()
.AddJsonOptions(option =>
{
option.JsonSerializerOptions.Converters.Add(new EmptyStringConverter());
});
Console Example
class Program
{
static void Main(string[] args)
{
var jsonSerializerOptions = new JsonSerializerOptions();
jsonSerializerOptions.Converters.Add(new EmptyStringConverter());
var json = JsonSerializer.Serialize(new Model() { FirstName = null }, jsonSerializerOptions);
}
}
public class EmptyStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Convert.ToString(reader.GetString(), CultureInfo.CurrentCulture);
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (writer == null)
throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue(value ?? "");
}
}
public class Model
{
public string FirstName { get; set; }
}
In .NET 5.0 this can be done by overriding JsonConverter<T>.HandleNull and returning true:
public class EmptyStringConverter : JsonConverter<string>
{
public override bool HandleNull => true;
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.TokenType == JsonTokenType.Null ? "" : reader.GetString();
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteStringValue(value ?? "");
}
For more, see Handle null values.
Demo fiddle here.
In .NET Core 3.x this is not implemented. From Handle null values in .NET Core 3.x:
Handle null values
By default, the serializer handles null values as follows:
For reference types and Nullable<T> types:
It does not pass null to custom converters on serialization.
It does not pass JsonTokenType.Null to custom converters on deserialization.
It returns a null instance on deserialization.
It writes null directly with the writer on serialization.
For non-nullable value types:
It passes JsonTokenType.Null to custom converters on deserialization. (If no custom converter is available, a JsonException exception is thrown by the internal converter for the type.)
This null-handling behavior is primarily to optimize performance by skipping an extra call to the converter. In addition, it avoids forcing converters for nullable types to check for null at the start of every Read and Write method override.
Try it
/// <summary>
/// Convert empty to null when read data json
/// </summary>
public class EmptyStringToNullConverter : JsonConverter<string>
{
/// <summary>
/// Override CanConvert method of JsonConverter
/// This instance only convert the string type.
/// </summary>
/// <returns></returns>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(string);
}
/// <summary>
/// Override ReadJson method of JsonConverter
/// Convert string null to empty
/// </summary>
/// <returns></returns>
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = (string)reader.GetString();
return value ?? String.Empty;
}
/// <summary>
/// Override WriteJson method of JsonConverter
/// </summary>
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
throw new NotImplementedException("Unnecessary");
}
}

Is polymorphic deserialization possible in System.Text.Json?

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

Categories