DEV Community

Cover image for How to Deserialize Internally Tagged JSON in C# with AoT compilation
Sby
Sby

Posted on

How to Deserialize Internally Tagged JSON in C# with AoT compilation

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 ItemModels.

[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(List<TextItem>))]
[JsonSerializable(typeof(List<ImageItem>))]
[JsonSerializable(typeof(List<ItemModel>))]
internal partial class SourceGenerationContext : JsonSerializerContext { }
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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
) ?? [];
Enter fullscreen mode Exit fullscreen mode
await JsonSerializer.SerializeAsync(
    stream,
    images,
    SourceGenerationContext.Default.ListImageItem
);
Enter fullscreen mode Exit fullscreen mode

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)