The problem
A closed class restricts derivation to its declaring assembly so that a switch over its direct descendants can be treated as exhaustive. Today a direct subtype may be less accessible than the closed base, in which case a consumer that can see the base but not the subtype can never write an exhaustive switch:
// Library assembly
public closed class Shape { }
public sealed class Circle : Shape { }
internal sealed class Hidden : Shape { } // less accessible than the public base
// Consumer assembly
string Describe(Shape shape) => shape switch
{
Circle => "circle",
// warning: switch is non-exhaustive. Pattern 'Shape' is not handled.
// But 'Hidden' is internal to the library and cannot be named here, so the consumer
// can never exhaust the switch by matching the derived types.
};
The current proposal accepts this and reports it only at the use site (the spec section linked above). LDM considered a declaration-time accessibility restriction on 2026-04-20 and declined: "We will not add any new restrictions related to accessibility."
The question
Should we revisit that decision and require, at declaration time, that every immediate descendant of a closed type have the same declared accessibility as the base?
Proposed rule
Every immediate descendant (direct subtype) of a closed type must be declared with the same declared accessibility as the closed base type; any other accessibility is a compile-time error.
public closed class Shape { }
public sealed class Circle : Shape { } // ok: same accessibility as 'Shape'
internal sealed class Hidden : Shape { } // error: a direct subtype of a closed type must have
// the same accessibility as its base
Considerations in favor
- Exhaustiveness then holds for every consumer that can see the base — which is the point of
closed.
- A declaration-time error is actionable at the hierarchy, versus a non-exhaustiveness warning in distant (possibly cross-assembly) consumer code that the consumer cannot fix.
- The direct descendants are part of the type's contract; hiding cases from consumers who can see the base contradicts what
closed advertises.
- STJ's closed-hierarchy support (
JsonSerializerOptions.InferClosedTypePolymorphism, runtime#129041) infers a $type discriminator (nameof(DerivedType)) for every derived type recorded in [IsClosedType(DerivedTypes = …)] (runtime#129009); permitting less-accessible descendants means an untrusted payload could instantiate an otherwise-inaccessible type/constructor — a potential security concern, which STJ is likely to decline to support regardless.
Scope and related considerations
file-local closed classes. file is a separate modifier, not a named accessibility level, so it needs explicit treatment: a file closed base should require file-local direct subtypes (and a more-visible base cannot have a file subtype). The current implementation has not exercised file closed class; that gap needs coverage regardless of the outcome here.
- Unspeakable or constraint-adding generic subtypes. The spec's Exhaustiveness when a subtype can't be used already notes that a generic subtype can be unspeakable for a given base instantiation — e.g.
class D2<V> : C<V[]> under closed class C<T>, where no D2<…> pattern can be written for an existential C<X>. The same reasoning behind this question applies: guaranteeing a usable construction wherever any base construction is used would mean disallowing such non-speakable derived generics at declaration, effectively restricting generic subtypes to "trivial" generics — ones that pass their type parameters directly to the base and add no constraints of their own. Best treated as a separate question.
- Consumption-site dependency on inaccessible metadata. Reference assemblies and trimming tools would have to special-case descendants of a
closed class to keep consumer pattern matching correct, and Roslyn metadata-import modes that hide inaccessible members interact poorly with consumption-site behavior that depends on inaccessible derived types. This is the same class of hole already known from private struct fields (managed/unmanaged determination — and thus pointer eligibility — depending on inaccessible fields); we would prefer not to enlarge it. The rule removes the dependency.
- Nested
closed levels. The rule applies to each closed type's own direct descendants independently; deeper hierarchies stay matchable.
- Type parameters constrained to a
closed type. Unaffected — that concerns generics and exhaustiveness, not accessibility.
Options
- Adopt the rule as a compile-time error — strongest guarantee; a
closed type is always exhaustively matchable by every consumer that can see it.
- Adopt the rule as a warning — flags the hazard at declaration without hard-breaking the pattern; weaker guarantee.
- Status quo (2026-04-20) — no declaration-time restriction; continue reporting non-exhaustiveness only at the use site.
Sub-decision for options 1 and 2: whether the rule extends to require matching file-locality, as described under Scope and related considerations.
[IsClosedType])The problem
A
closedclass restricts derivation to its declaring assembly so that aswitchover its direct descendants can be treated as exhaustive. Today a direct subtype may be less accessible than theclosedbase, in which case a consumer that can see the base but not the subtype can never write an exhaustive switch:The current proposal accepts this and reports it only at the use site (the spec section linked above). LDM considered a declaration-time accessibility restriction on 2026-04-20 and declined: "We will not add any new restrictions related to accessibility."
The question
Should we revisit that decision and require, at declaration time, that every immediate descendant of a
closedtype have the same declared accessibility as the base?Proposed rule
Considerations in favor
closed.closedadvertises.JsonSerializerOptions.InferClosedTypePolymorphism, runtime#129041) infers a$typediscriminator (nameof(DerivedType)) for every derived type recorded in[IsClosedType(DerivedTypes = …)](runtime#129009); permitting less-accessible descendants means an untrusted payload could instantiate an otherwise-inaccessible type/constructor — a potential security concern, which STJ is likely to decline to support regardless.Scope and related considerations
file-local closed classes.fileis a separate modifier, not a named accessibility level, so it needs explicit treatment: afileclosedbase should requirefile-local direct subtypes (and a more-visible base cannot have afilesubtype). The current implementation has not exercisedfile closed class; that gap needs coverage regardless of the outcome here.class D2<V> : C<V[]>underclosed class C<T>, where noD2<…>pattern can be written for an existentialC<X>. The same reasoning behind this question applies: guaranteeing a usable construction wherever any base construction is used would mean disallowing such non-speakable derived generics at declaration, effectively restricting generic subtypes to "trivial" generics — ones that pass their type parameters directly to the base and add no constraints of their own. Best treated as a separate question.closedclass to keep consumer pattern matching correct, and Roslyn metadata-import modes that hide inaccessible members interact poorly with consumption-site behavior that depends on inaccessible derived types. This is the same class of hole already known from private struct fields (managed/unmanaged determination — and thus pointer eligibility — depending on inaccessible fields); we would prefer not to enlarge it. The rule removes the dependency.closedlevels. The rule applies to eachclosedtype's own direct descendants independently; deeper hierarchies stay matchable.closedtype. Unaffected — that concerns generics and exhaustiveness, not accessibility.Options
closedtype is always exhaustively matchable by every consumer that can see it.Sub-decision for options 1 and 2: whether the rule extends to require matching
file-locality, as described under Scope and related considerations.