Skip to content

[API Proposal]: JsonPolymorphicAttribute.InferClosedTypePolymorphism #130809

Description

@eiriktsarpalis

Background and motivation

JsonSerializerOptions.InferClosedTypePolymorphism and JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism (approved in #129041 and implemented by #130808) let the serializer take the derived-type list for a closed hierarchy from compiler-emitted metadata instead of requiring [JsonDerivedType(...)] entries.

Both switches are global: they turn inference on for every closed hierarchy handled by that JsonSerializerOptions instance or JsonSerializerContext. There is no way to opt a single hierarchy in.

The concrete gap is a library or application with one closed hierarchy that should be serialized polymorphically via inference, while the same serializer options or context handles other closed types that use closed-hierarchy semantics for purposes such as pattern matching and should not start emitting $type payloads. Today the author must either enable inference globally or hand-write [JsonDerivedType] registrations for the one hierarchy.

This proposes a declaration-scoped opt-in that mirrors the global flag's semantics but applies only to the type carrying [JsonPolymorphic].

The original #129041 proposal included an attribute-scoped shape named InferDerivedTypes. API review moved it to the options and source-generation surfaces so it could apply to multiple types at once (review notes). This proposal adds the per-declaration variant as a complement: the global switch opts many types in; the attribute opts exactly one in.

Related: #129041, #130808.

API Proposal

namespace System.Text.Json.Serialization;

public sealed partial class JsonPolymorphicAttribute : JsonAttribute
{
    // EXISTING
    // public JsonPolymorphicAttribute() { }
    // public bool IgnoreUnrecognizedTypeDiscriminators { get; set; }
    // public Type? TypeClassifier { get; set; }
    // public string? TypeDiscriminatorPropertyName { get; set; }
    // public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; set; }

    public bool InferClosedTypePolymorphism { get; set; }
}

Semantics

  • When true on a base whose compiler metadata marks it as a closed hierarchy, one JsonDerivedType is registered per member of the closed set. Each derived type's simple name, equivalent to nameof, is used as its type discriminator.
  • Effective inference is global option OR attribute flag. The attribute enables inference for its declaration even when the serializer/source-generation option is false. If global inference is already enabled, the attribute is redundant rather than erroneous.
  • Explicit [JsonDerivedType] registrations win wholesale. Matching Infer System.Text.Json polymorphism from closed type hierarchies #130808, inference is skipped when the type has any explicit registration; inferred and explicit lists are not merged.
  • An inferred derived type must be at least as visible as the base; otherwise reflection throws InvalidOperationException and source generation reports the corresponding diagnostic.
  • Both the reflection resolver and source generator honor the flag.

Open questions for API review

  1. As a bool, this can only opt in. An unset value is indistinguishable from explicitly setting false, so it cannot suppress inference for one type when the global switch is enabled. Is opt-in-only sufficient, or is a per-type suppression mechanism needed?
  2. The global switch is a no-op for a type without closed-hierarchy metadata. Since this attribute expresses explicit per-type intent, should applying it to a non-closed base instead throw or produce a source-generation diagnostic? The original attribute concept in [API Proposal]: Add STJ support for closed hierarchies #129041 threw InvalidOperationException for this case.

Prototype: none; this is a declaration-scoped form of the behavior implemented by #130808.

API Usage

Opt one hierarchy into inferred polymorphism without enabling it for other closed types handled by the same serializer:

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonPolymorphic(InferClosedTypePolymorphism = true)]
closed class Animal
{
    public required string Name { get; set; }
}

sealed class Dog : Animal { public required string Breed { get; set; } }
sealed class Cat : Animal { public int Lives { get; set; } }

// Not opted in, so this hierarchy remains non-polymorphic.
closed class Shape { }
sealed class Circle : Shape { }

string json = JsonSerializer.Serialize<Animal>(new Dog { Name = "Rex", Breed = "Lab" });
// {"$type":"Dog","Name":"Rex","Breed":"Lab"}

Alternative Designs

  • Use the global JsonSerializerOptions or JsonSourceGenerationOptionsAttribute switch. This is all-or-nothing across the serializer or context and cannot target one hierarchy.
  • Hand-write [JsonDerivedType] registrations. This reintroduces the boilerplate and drift that closed-hierarchy inference removes.
  • Use a constructor parameter. JsonPolymorphicAttribute currently exposes configuration through named properties; another property is consistent with that shape.

Risks

This is an additive settable bool on an existing sealed attribute, defaulting to false. It introduces no behavior change unless explicitly set. When enabled, it reuses the inference behavior implemented by #130808.

Note

This issue was drafted with GitHub Copilot.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions