Skip to content

Add System.Text.Json union support#128162

Merged
eiriktsarpalis merged 10 commits into
dotnet:mainfrom
eiriktsarpalis:feature/json-unions
May 21, 2026
Merged

Add System.Text.Json union support#128162
eiriktsarpalis merged 10 commits into
dotnet:mainfrom
eiriktsarpalis:feature/json-unions

Conversation

@eiriktsarpalis
Copy link
Copy Markdown
Member

@eiriktsarpalis eiriktsarpalis commented May 13, 2026

Summary

Implements System.Text.Json support for C# union-like shapes:

  • adds union contract metadata (JsonTypeInfoKind.Union, JsonUnionCaseInfo, JsonUnionInfoValues<T>, union constructor/deconstructor hooks)
  • adds JsonUnionAttribute, JsonTypeClassifier, and classifier factory/context APIs for union and polymorphic type classification
  • adds union converter support for reflection and source-generated metadata
  • teaches the source generator to discover union case constructors and emit union metadata
  • adds reflection, source-generation, and source-generator unit coverage for union and structural classifier scenarios

Fixes #127299.

Adds System.Text.Json contract metadata, converters, source generation support, and tests for C# union type serialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds System.Text.Json support for C# union-like types, including runtime metadata, custom JSON type classifier APIs, converter support, source-generation plumbing, diagnostics, schema handling, and test coverage.

Changes:

  • Adds union metadata/converter infrastructure and classifier APIs for unions and polymorphic types.
  • Extends source generation to emit union and polymorphism/classifier metadata plus diagnostics.
  • Adds reflection/source-generation tests and updates generated baselines/resources.

Reviewed changes

Copilot reviewed 163 out of 163 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/libraries/System.Text.Json/src/System.Text.Json.csproj Includes new union/classifier source files and shared compiler attributes.
src/libraries/System.Text.Json/ref/System.Text.Json.csproj Adds shared compiler attribute references for ref build.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonUnionAttribute.cs Adds union customization attribute.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicAttribute.cs Adds polymorphic classifier hook.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonTypeClassifier*.cs Adds classifier delegate, context, factory, and kind APIs.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueType.cs Adds internal JSON value-shape flags.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Union/* Adds union converter and converter factory.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/* Advertises supported JSON value shapes for built-in converters.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/*Union* Adds union metadata values and reflection/source-gen metadata support.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo*.cs Adds union constructor/deconstructor, union cases, classifier resolution, and kind support.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonObjectInfoValuesOfT.cs Adds generated polymorphism/classifier metadata slots.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonCollectionInfoValuesOfTCollection.cs Adds generated polymorphism/classifier metadata slots for collections.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices*.cs Adds union creation and generated polymorphism metadata wiring.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver*.cs Adds reflection resolver union discovery and classifier wiring.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor*.cs Adds constructor/getter helpers used by union metadata.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions*.cs Adds classifier list option and cache/equality integration.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs Adds polymorphic classifier dispatch during metadata handling.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter*.cs Extends converter strategy/value-shape and classifier polymorphic handling.
src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs Adds schema generation for union contracts.
src/libraries/System.Text.Json/src/Resources/Strings.resx Adds runtime union/classifier error messages.
src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs Adds source-generation classifier option.
src/libraries/System.Text.Json/gen/* Adds source-generator model, diagnostics, parser/emitter, resources, and target updates for unions/classifiers.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/* Adds reflection serializer union/classifier test wrappers and project includes.
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/* Adds source-generation union/classifier/polymorphism tests and project includes.
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/* Adds diagnostics/output support and baseline updates.
docs/project/list-of-diagnostics.md Documents new SYSLIB1227/SYSLIB1228 diagnostics.

Comment thread src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Add netcoreapp type forwards for compiler support attributes embedded in downlevel System.Text.Json assets so package ApiCompat sees matching surface area across target frameworks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 17:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings May 14, 2026 17:54
Clarify classifier documentation and align union constructor dispatch between reflection and source generation. Reject byref union case constructors and add regression coverage for overlapping and nullable cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove emitter constants that only name generated source members or locals and inline those names directly in the emitted source strings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Comment thread src/libraries/System.Text.Json/tests/Common/UnionTests.cs
Copy link
Copy Markdown
Member

@tarekgh tarekgh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments and suggestions. LGTM, otherwise.

eiriktsarpalis and others added 2 commits May 19, 2026 18:44
…-shape map

Source-generator change: in JsonSourceGenerator.Parser, types implementing IDictionary<,>/IReadOnlyDictionary<,>/IDictionary now map to JsonValueType.Object before the IEnumerable<T> check, matching the runtime ConverterStrategy fallback. Previously Dictionary<TKey,TValue> was incorrectly classified as Array because Dictionary<,> implements IEnumerable<KeyValuePair<,>>, producing a spurious SYSLIB1227 ambiguity for a union over a List<T> and a Dictionary<,> case and diverging from reflection-mode runtime behavior.

EnumConverter.GetSupportedJsonValueTypes adds Debug.Assert(allowsString || allowsNumber) before the pattern match, since the EnumConverter constructor guarantees at least one of those flags is set.

Tests:

- Common/StructuralJsonTypeClassifierTests: new ArrayOrDictionaryUnion(List<int>, Dictionary<string,int>) covering shape-based dispatch (also registered in both source-gen contexts).

- JsonSourceGeneratorDiagnosticsTests: new UnionWithListAndDictionaryCases_CompilesWithoutWarning verifying no SYSLIB1227 for a (List<int>, Dictionary<string,int>) union.

- Common/UnionTests: new NullableEnumUnionCase_SchemaIncludesNullInTypeArray verifying that a Color? case in a union produces a case sub-schema whose type array includes both 'integer' and 'null'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ceHandler.Preserve

Production:

- JsonSerializer.Read.HandleMetadata: the classifier branch no longer pre-sets
  MetadataPropertyName.Type. Pre-setting that bit caused the existing
  `no metadata before $ref '' guard to throw spuriously for classifier-based
  polymorphic class types whose payload is a $ref-only object. Instead the
  branch records the classifier result on state.PolymorphicResolvedType and
  defers $ref resolution to the normal metadata loop so the reference resolver
  can return the previously-deserialized instance. A peek before invoking the
  classifier skips classification entirely for $ref-only payloads to avoid
  asking the classifier to identify a type from a metadata-only object.

- The four ObjectDefault / ObjectWithParameterizedConstructor / JsonCollection /
  JsonDictionary OnTryRead caller gates now enter ResolvePolymorphicConverter
  when either the Type metadata bit OR state.PolymorphicResolvedType is set,
  so the classifier-resolved type is honored without the metadata bit being
  forced on. The matching Debug.Assert in JsonConverter.MetadataHandling is
  relaxed to the same condition.

Tests:

- PolymorphicTests.TypeClassifier: new Classifier_WithReferenceHandlerPreserve_PreservesReferences
  round-trips ClassifiedAnimalBase[] {dog, dog} under ReferenceHandler.Preserve
  with a "kind"-based classifier (the modifier sets PolymorphismOptions.TypeDiscriminatorPropertyName
  so writer and classifier agree on the property name). Asserts both elements
  resolve to the same instance.

- StructuralJsonTypeClassifierTests (test assembly):
  - NonPublicFactoryUnion + internal sealed NonPublicClassifierFactory with an
    explicit private ctor + ReflectionMode_NonPublicClassifierFactory_IsRejected
    locks in that [JsonUnion(TypeClassifier = ...)] fails resolution for a
    factory whose accessible constructors are not public.
  - StructuralJsonTypeClassifierTests_AsyncStreamWithSmallBuffer and
    _SyncStreamWithSmallBuffer wrappers exercise the classifier path across
    the streaming TrySkipPartial retry boundary.

Test-infra fixes surfaced by the new wrappers:

- tests/Common/StructuralJsonTypeClassifierTests: replaced reader.Skip() with
  reader.TrySkip() in ScoreArray / ScoreObject / ScoreDictionary. The runtime
  passes the original (non-final) reader to user classifiers even after the
  object has been buffered via TrySkipPartial, so Skip() throws
  InvalidOperationException in streaming scenarios.

- JsonSerializerWrapper.Reflection: replaced the racy TryGetValue + Add pair in
  JsonSerializerOptionsSmallBufferMapper with the atomic
  ConditionalWeakTable.GetValue(key, factory) so concurrent small-buffer
  wrapper tests sharing the same source options don't collide.

Addresses tarekgh PR feedback items 2, 5, 9, 10.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eiriktsarpalis eiriktsarpalis enabled auto-merge (squash) May 19, 2026 16:57
…Type bitmask

JsonValueType is already a [Flags] enum, so the per-JsonTypeInfo set of ambiguous JSON value shapes can be stored as a single JsonValueType value (None = no ambiguities) instead of a HashSet<JsonValueType>?. This eliminates a per-union-type heap allocation and lookup, replacing them with an enum bitwise-OR populate path and an '& flag != 0' check at deserialize time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis added a commit to eiriktsarpalis/runtime that referenced this pull request May 20, 2026
Responds to PR dotnet#128162 review feedback from @PranavSenthilnathan
(dotnet#128162 (comment)).

Production:
- DefaultJsonTypeInfoResolver.Union.cs: when a [Union] type declares public
  `bool TryGetValue(out CaseType)` overloads, the reflection-based
  deconstructor probes them most-derived-first before falling back to the
  runtime-type `ResolveUnionCase` lookup. The constructor is unchanged.
  Discovery uses `Type.GetMember("TryGetValue", MemberTypes.Method, ...)`
  to give the trimmer a strong signal and avoid filtering the full method
  set.
- MemberAccessor + Reflection/ReflectionEmit/ReflectionEmitCaching variants:
  add a single `CreateUnionTryGetValueAccessor<TUnion>` factory that
  returns one chained delegate over all TryGetValue overloads. The
  Reflection.Emit variant emits one `DynamicMethod` that probes each
  overload directly without intermediate boxing or closures. The
  Reflection-only variant uses `Delegate.CreateDelegate` with a typed
  per-case branch (struct vs class) so the union is unboxed exactly once.

Tests (tests/Common/UnionTests.cs):
- `CustomDiscriminatedAnimalUnion` + `CustomDiscriminatedAnimalUnion_NoConvention`
  hand-rolled unions wrap (Animal, Dog) hierarchies with and without
  TryGetValue overloads and assert the most-derived-declared-case rule
  that motivated the user's rejection of reverse-ordering in the same
  thread.
- `CustomDiscriminatedScalarUnion` covers disjoint scalar shapes.
- The Dog-upcast / Lab tests that exercise the reflection-only TryGetValue
  chaining are gated to the reflection path; source-gen's plain
  `value switch` doesn't honor TryGetValue and would observe different
  behavior. Aligning source-gen with the reflection convention is a
  separate follow-up.

Build + focused sweeps green (0 warn / 0 err):
- Reflection: 39/39 CustomDiscriminated, 175/175 Union, 115/115 Structural,
  5565/5565 Polymorphic.
- SourceGen (Roslyn 4.4): 52/52 CustomDiscriminated, 232/232 Union,
  32/32 Structural, 27/27 Polymorphic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis and others added 2 commits May 20, 2026 21:04
…oxing TryGetValue accessor

- Add WriteStack depth guard in JsonUnionConverter.OnTryWrite to detect
  cyclic union references (unions delegate inline without structural JSON
  tokens, so the writer depth alone cannot catch cycles).
- Add non-boxing TryGetValue chained accessor via MemberAccessor
  (Reflection.Emit + Reflection-only implementations).
- Restore PopulateUnionDeconstructor to private static void with
  ResolveUnionCase as the fallback deconstruction path.
- Add union RecursiveNat(bool, RecursiveNat) tests validating that
  nested unions flatten on serialization and deserialize as single-level
  wrappers.
- Add [Union] class SelfReferentialUnion tests validating that cyclic
  object graphs throw JsonException at MaxDepth.
- Add CustomDiscriminatedAnimalUnion / ScalarUnion tests covering
  TryGetValue convention dispatch and runtime-type dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eiriktsarpalis
Copy link
Copy Markdown
Member Author

/ba-g test failures are unrelated

@eiriktsarpalis eiriktsarpalis merged commit b521e15 into dotnet:main May 21, 2026
90 of 96 checks passed
@eiriktsarpalis eiriktsarpalis deleted the feature/json-unions branch May 21, 2026 06:10
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview6 milestone May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: System.Text.Json union type support

4 participants