In this quick tutorial we'll see how to set up JSON serialization and deserialization for an internally tagged object in C# System.Text.Json
using AoT-friendly polymorhpism source generators.
Unlike reflection and writing a custom converter, source generators makes this both easy and performant.
1. Declare Context
First, you need a class holding the context for your serialization and deserialization for which source will be generated.
For that, you need to inherit from JsonSerializerContext
and declare your options with JsonSourceGenerationOptions
.
Then you need to declare which types you want to serialize and deserialize. Here we have ItemModel
base class and TextItem
and ImageItem
derived classes. Using typeof(List<ItemModel>)
will allow for deserializing both a single ItemModel
object and a list of ItemModel
s.
[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(List<TextItem>))]
[JsonSerializable(typeof(List<ImageItem>))]
[JsonSerializable(typeof(List<ItemModel>))]
internal partial class SourceGenerationContext : JsonSerializerContext { }
2. Create Type Discriminators
You need a way to tell JSON deserializer that your base class has derived implementations.
For that we'll use JsonPolymorphic
and JsonDerivedType
metadata on the base class.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(TextItem), typeDiscriminator: "text")]
[JsonDerivedType(typeof(ImageItem), typeDiscriminator: "image")]
public abstract class ItemModel { }
C# uses type disciminator property "$type"
by default but here we're declaring it explicitly.
Then we point to the derived types and give the type discriminator "text"
and "image"
. However, you're not limited to using strings here and can use numbers to, for example, save on size.
2.1. Optional. Serializing Derived Types
While serializing and deserializing ItemModel
will work as intended, serializing TextItem
and ImageItem
directly will not generate a type discriminator, which is something you may want to do.
For that we can simply configure the derived type to be considered derived from itself.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(TextItem), typeDiscriminator: "text")]
public class TextItem : ItemModel { }
Now we have repeating strings in base class and derived class, which can be refactored into constants.
internal static class Constants
{
public const string Discriminator = "$type";
public const string TextDiscriminator = "text";
public const string ImageDiscriminator = "image";
}
[JsonPolymorphic(TypeDiscriminatorPropertyName = Constants.Discriminator)]
[JsonDerivedType(typeof(TextItem), typeDiscriminator: Constants.TextDiscriminator)]
[JsonDerivedType(typeof(ImageItem), typeDiscriminator: Constants.ImageDiscriminator)]
public abstract class ItemModel { }
3. Pass the Context
Now all we need to do is pass the generated context to the serializer/deserializer.
var items = await JsonSerializer.DeserializeAsync<List<ItemModel>>(
stream,
SourceGenerationContext.Default.ListItemModel
) ?? [];
await JsonSerializer.SerializeAsync(
stream,
images,
SourceGenerationContext.Default.ListImageItem
);
Conclusion
And that's it for this tutorial on how to work with internally tagged polymorphic JSON objects in C#.
Feel free to share thoughts and questions in the comments
Top comments (0)