Filter closed-derived registrations that don't match the base specialization (JsonPolymorphism follow-up)#129294
Conversation
…ization PR #127318 introduced an open-generics polymorphism unifier that surfaced several pathological registration patterns the existing throw/warn surface didn't handle cleanly. This commit adds targeted handling for the closed- derived case and adds coverage that demonstrates the pre-existing open-derived diagnostic behavior for the remaining patterns. ## What changed * Closed-derived `[JsonDerivedType]` registrations whose base spec doesn't match the current closed base (e.g. `Cat:Animal<int>` registered on `Animal<string>` via `Animal<T>`) are now silently filtered. Reflection drops them via `IsAssignableFrom`; source-gen drops them via `HasImplicitConversion`. Previously the registration leaked past the open-generic resolver and either threw at `PolymorphicTypeResolver.UpdateDerivedTypes` or emitted a confusing diagnostic. * When closed-derived filtering empties the derived-types list AND the user did not write `[JsonPolymorphic]`, the type silently degrades to non- polymorphic. If `[JsonPolymorphic]` is present the user has explicitly opted in, so the normal "no derived types" path runs. ## What was kept * Open-derived failures (NotAssignable, UnboundParameter, UnificationFailed, ConstraintViolation, AmbiguousMatch) and open-derived registrations on non-generic bases continue to throw `InvalidOperationException` (reflection) and emit `SYSLIB1229` (source-gen) exactly as in PR #127318. These are genuine misregistrations that warrant a loud diagnostic. ## Tests * New reflection tests covering the three pathological scenarios called out in the PR feedback: closed-derived mismatch with int/string/bool specializations, `where T : IEnumerable<int>` constraint failing for `Base<string>`, and `Cat<T,T2>` on `Animal<T>`. * Mixed-scenario tests verifying that filtered closed derived plus a surviving open derived continue to round-trip correctly. * New source-gen runtime tests for the same scenarios, using `#pragma warning disable SYSLIB1229` around the bad open-derived registrations to verify that the warning is suppressible and the post- warning runtime metadata is benign. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @dotnet/area-system-text-json |
There was a problem hiding this comment.
Pull request overview
This PR adjusts System.Text.Json polymorphism metadata generation for generic base specializations so that closed [JsonDerivedType] registrations that don’t match the current constructed generic base are silently filtered (instead of causing exceptions / diagnostics). It also ensures that when filtering leaves no derived types and there was no explicit [JsonPolymorphic], the specialization is treated as non-polymorphic to avoid downstream “empty options” failures.
Changes:
- Reflection resolver: filter out mismatched closed derived registrations for generic base specializations and demote to non-polymorphic when filtering empties the list (unless
[JsonPolymorphic]was explicitly declared). - Source generator parser: mirror the closed-derived filtering for generic base specializations.
- Add reflection + source-gen runtime tests covering specialization-specific closed-derived registrations, constraint/unification failure behaviors, and mixed open/closed scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | Filters mismatched closed derived types for generic base specializations and demotes to non-polymorphic when filtering empties derived types without explicit [JsonPolymorphic]. |
| src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs | Adds source-gen-side filtering of mismatched closed derived registrations for generic base specializations (mirrors reflection behavior). |
| src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs | Adds reflection-based test coverage for specialization-specific filtering and existing open-derived failure modes. |
| src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs | Adds source-gen runtime tests validating emitted metadata behavior (filtered derived types and non-polymorphic demotion). |
This comment has been minimized.
This comment has been minimized.
- Source-gen filter: classify conversion via Compilation.ClassifyConversion
so identity+implicit-reference (matching System.Type.IsAssignableFrom)
is enforced instead of HasImplicitConversion, which would have admitted
user-defined implicit operators that the runtime resolver rejects.
Drops the INamedTypeSymbol pattern on the derived side so other closed
shapes (e.g. arrays) are filtered consistently with reflection.
- Test renames: ClosedDerivedTypes_MismatchedBaseSpecialization_AreFilteredOn{Int,String}
-> ...OnSpecAnimalOf{Int,String} to match the SpecAnimal<T> fixture name.
- Drop System.Collections.Generic.* qualifications in test code (already imported).
- New test: ClosedDerivedTypes_AllFilteredButPolymorphicAttributeExplicit_Throws
covering the [JsonPolymorphic]-is-explicit branch in PopulatePolymorphismMetadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Note This review was generated by Copilot. 🤖 Copilot Code Review — PR #129294Holistic AssessmentMotivation: Justified. PR #127318 introduced open-generics polymorphism support but left a gap: closed Approach: Sound and minimal. The fix correctly splits behavior between closed-derived (silent filter for generic bases) and open-derived (loud failure preserved) at both the reflection and source-gen layers. The backward-iteration pattern, demotion logic, and Summary: ✅ LGTM. The code is correct, the reflection/source-gen parity is maintained, previous review feedback has been addressed (ClassifyConversion instead of HasImplicitConversion, test renames, explicit Detailed Findings✅ Correctness — Core filtering logic is soundReflection side (
Source-gen side (
Demotion logic (
✅ Tests — Comprehensive scenario coverageThe test matrix covers:
Source-gen tests appropriately use narrowly-scoped 💡 Minor observation — source-gen test gap for
|
Note
The code in this PR was AI/Copilot-generated. The PR author reviewed the changes before opening this PR.
Follow-up to #127318
While stress-testing the open-generics polymorphism support added in #127318, a few pathological registration shapes surfaced where the source generator threw
InvalidOperationException(or emittedSYSLIB1229) on perfectly legitimate user code. The common pattern: the user attaches closed[JsonDerivedType]attributes to an open generic base, intending each attribute to apply to a specific specialization of that base.Example that motivated this PR
Before this change:
Animal<int>would crash becauseDog(assignable only toAnimal<string>) failed theIsAssignableFromcheck insidePolymorphicTypeResolver.Animal<bool>would crash because neither closed derived was assignable to it.After this change:
Animal<int>resolves with{ Cat }as derived types.Animal<string>resolves with{ Dog }as derived types.Animal<bool>silently demotes to non-polymorphic (no[JsonPolymorphic]was declared, so emitting empty polymorphism options would itself throw).The same fix is mirrored in both the reflection resolver (
DefaultJsonTypeInfoResolver.Helpers.ResolveOpenGenericDerivedTypes) and the source-gen parser (JsonSourceGenerator.Parser.ProcessTypeCustomAttributes).What is NOT changed
All open-derived failure modes from #127318 keep their existing behavior —
NotAssignable,UnboundParameter,UnificationFailed,ConstraintViolation, andAmbiguousMatchstill throw at reflection time and still emitSYSLIB1229at source-gen time. The rationale: an open[JsonDerivedType(typeof(Foo<>))]declares intent to participate at this generic position, so failure to do so is a real misregistration the user should learn about. A closed derived attached to an open base, by contrast, structurally cannot apply to every specialization — silent filtering is the only sensible behavior.The behavioral change is therefore confined to:
InvalidOperationException/ emitsSYSLIB1229JsonDerivedTypeattributes filtered + no explicit[JsonPolymorphic]SYSLIB1229(unchanged)SYSLIB1229(unchanged)Implementation notes
derivedTypesbackwards soRemoveAt(i)is safe.[JsonPolymorphic]was not explicitly declared, nulls out the options before callingSetPolymorphismOptions. When the user did write[JsonPolymorphic], intent is preserved and the downstream "no derived types specified" error is left to surface.Compilation.HasImplicitConversionto perform the same assignability check at compile time.Tests
SpecAnimal_ResolvesPerSpecialization,ConstrainedBase_*,ExtraParam_*, andMixed_OpenAndClosedDerived_*cases toPolymorphicTests.CustomTypeHierarchies.cscovering all the patterns from the original issue.System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs.#pragma-suppressedSYSLIB1229for the contexts that intentionally exercise the open-derived failure paths to keep the fixture compileable.PolymorphicTests*6136/6136,JsonPolymorphismOptionsTests35/35, source-gen runtimePolymorphismTests16/16,JsonSourceGeneratorDiagnosticsTests86/86 (Roslyn 4.4) + 81/81 (Roslyn 3.11).Branch name
Note: the local branch name still carries the
pr/127318/copilot/...prefix because the session's PR association couldn't be reset prior to opening this PR. The branch contents are completely disentangled from #127318 — it sits directly on top oforigin/mainwith a single commit.cc @eiriktsarpalis