Saving C# 15 unions with System.Text.Json in .NET 11
Part 4 of NimblePros' domain modeling series walks through saving C# 15 unions and closed hierarchies with System.Text.Json on the .NET 11 RC1 SDK. It shows where the built-in classifiers are enough and where a hand-made "kind" discriminator is still needed. Useful for any developer persisting polymorphic domain state to JSON.
Key points
- A structural classifier is enough for union cases whose properties differ in shape.
- Cases with identical shapes cannot be read back by default on RC1 and throw a JsonException.
- Closed hierarchies still need JsonPolymorphic and every JsonDerivedType listed by hand.
- The workaround uses a contract modifier and a type classifier instead of a custom JsonConverter.
- Unknown "kind" values fail loudly with an exception that lists the valid kinds.
- With the source generator, the classifier must be registered in TypeClassifiers to satisfy SYSLIB1227.
The author looks at what goes wrong when a game's state is written to disk and read back: a save file is where type safety leaks out, because a union case becomes a plain JSON object with no marker of its type. System.Text.Json 11 recognizes unions, but on RC1 it cannot read most of them back by default. When the cases have different shapes, one attribute with a structural classifier is enough. Closed hierarchies still need their derived types listed by hand under JsonPolymorphic.
For cases that share the same shape, the workaround adds a "kind" property through a contract modifier and identifies the case with a custom type classifier. This avoids writing a full JsonConverter, and unknown kinds fail with a clear exception instead of loading as an empty value. The approach is tagged as temporary, so it can be removed once a built-in union discriminator ships.
Real-world use
- A game or workflow app that saves its state to a JSON file and resumes from the same point the next day.
- A web API that receives command types as JSON, where several command cases have identical property shapes.
- An AOT-friendly, source-generated .NET app that must persist a polymorphic domain model without custom converters.
Why it matters
Knowing which union shapes the built-in classifiers handle tells you when you can skip hand-written converters. On RC1, identically shaped cases need your own "kind" discriminator.