ASP.NET Core in .NET 11 provides practical support for using union types and closed hierarchies with System.Text.Json, allowing a value to represent more than one type or a defined family of types. This support extends to Minimal APIs, MVC controllers, SignalR, Blazor, and OpenAPI documentation, with important differences in how values are read and written and in the limits of parameter binding.
When is a union appropriate?
A union is suitable for contracts that accept independent alternatives without requiring a JSON wrapper or a discriminator field. Examples include a value that can be either an integer or a string, or a setting that accepts either a Boolean value or an object containing more detailed options. During serialization, the active case is written directly in its natural form; no special union wrapper or discriminator is added.
Cases that can be clearly distinguished from the JSON structure are serialized automatically. However, if cases share the same structure, such as a union containing Cat and Dog objects, System.Text.Json cannot determine the type from the JSON token alone. In this situation, you can provide a custom JsonTypeClassifier to select the correct case. The same applies to some request contracts that accept a number or a string: the web JSON settings allow numbers to be read from strings, which may make the text interpretable as either case.
Closed hierarchies differ from unions
Using the closed modifier does not mean that polymorphic serialization will be enabled automatically. When handling a concrete derived type such as PaymentAuthorized, System.Text.Json treats it like any other type and produces JSON containing paymentId and amount without a discriminator.
When the base type PaymentEvent is used in an API, the derived type to create must be identified. This can be enabled on the base type by using JsonPolymorphic(InferClosedTypePolymorphism = true), or through JsonSerializerOptions.InferClosedTypePolymorphism. System.Text.Json then infers the derived types in the closed hierarchy during serialization and uses their names as identifiers, such as $type with the value PaymentAuthorized.
The choice between the two models depends on the nature of the contract: a union represents alternatives that may not belong to a single family, while closed hierarchies suit a related set of events or entities that require a clear discriminator.
Support across ASP.NET Core layers
In Minimal APIs, a union can be used as a request-body parameter or as a return type, either in the usual execution path or through the Request Delegate Generator, with matching behavior between the two paths. A union can also appear inside another model, as an element in IAsyncEnumerable<T>, or within a container that uses AsParameters, and ConfigureHttpJsonOptions settings continue to affect serialization.
MVC controllers support unions as action parameters and return types, including Task<TUnion> and ValueTask<TUnion>. Because MVC uses JsonSerializerDefaults.Web settings, the ambiguity between numbers and strings in HTTP input applies to it as well.
In SignalR, support works through JsonHubProtocol for parameters, return values, and streaming elements. A union of int or string does not require a classifier when using this protocol, because JsonHubProtocol does not treat a JSON string token as ambiguous in this case. However, unions whose cases share the StartObject token, such as UnionPet(Cat, Dog), still require a classifier. SignalR does not support this functionality when using the MessagePack or Newtonsoft.Json hub protocols.
Blazor and OpenAPI
In-process Blazor component parameters use direct assignment and therefore do not require serialization. However, JavaScript interop, persisted component state, and parameters during prerendering pass through System.Text.Json and follow the same union rules. For example, a union can be passed to JavaScript as a Boolean or an options object, depending on the active case, without adding a wrapper or discriminator.
In OpenAPI, a union is represented using an anyOf schema containing one type for each case. The cases reuse the same Cat and Dog component names when they appear as independent types. This differs from polymorphic types that carry a discriminator and have their schemas promoted with distinct names. ApiExplorer can also represent multiple response types for the same status code and content type within anyOf.
Limitations to consider
Union support relies on System.Text.Json and therefore does not work with binding sources that do not pass through JSON parsing. These include query-string values, route values, headers, and form fields. A value such as ?id=42 is not enough to determine whether the intended type is int, string, or Guid. In Blazor, the limitation applies to [SupplyParameterFromQuery] and form binding using [SupplyParameterFromForm]. The source indicates that binding unions from these sources remains under investigation and feedback collection.
Editorial reading: The main value here is not adding a new JSON format, but unifying the handling of multi-case contracts across broad portions of the .NET 11 ecosystem. However, the success of the design depends on whether the cases can be distinguished and on the data source; therefore, a classifier or discriminator should be defined early, and serialization support should not be assumed to mean support for every parameter-binding method.