Context
PR #213 registers migration as a resolver at the front of TypeInfoResolverChain and remembers the registration per options instance (MigrationScope, keyed in a ConditionalWeakTable) and per chain object (ScopesByChain, which is what a copy made with new JsonSerializerOptions(original) resolves through until it changes its own chain).
A copy that replaces or extends its resolver before any migration lookup runs on it, while the migration entry sits behind an application-defined resolver, carries nothing that identifies its origin:
var copy = new JsonSerializerOptions(original);
copy.TypeInfoResolver = new ForwardingResolver(copy.TypeInfoResolverChain[0]);
copy.AddJsonMigrationSupport(); // registers again with the configuration it passes
The chain walk cannot see through the wrapper, probing the wrapper would call a user's resolver during registration (and freeze a DefaultJsonTypeInfoResolver's Modifiers), and inferring from chain shape was shown to misattribute registrations between independent options. So such a copy registers again, like any options whose chain no longer shows the entry.
Idea
On .NET 11 the copy constructor also copies TypeClassifiers, and AddJsonMigrationSupport() adds a JsonMigratableUnionTypeClassifier instance there. That instance is ours, is added only by registration, and is never shared between independently created options, so it is sound provenance: give it an internal constructor that carries the JsonMigrationTypeInfoResolver, and let MigrationScope.FindCached look for it before structural discovery. Copies then keep their registration on .NET 11 regardless of what they do to their chain, while the documented re-registration stays the .NET 10 behavior.
Out of scope for #213 because it makes behavior differ by target framework and needs its own documentation.
Context
PR #213 registers migration as a resolver at the front of
TypeInfoResolverChainand remembers the registration per options instance (MigrationScope, keyed in aConditionalWeakTable) and per chain object (ScopesByChain, which is what a copy made withnew JsonSerializerOptions(original)resolves through until it changes its own chain).A copy that replaces or extends its resolver before any migration lookup runs on it, while the migration entry sits behind an application-defined resolver, carries nothing that identifies its origin:
The chain walk cannot see through the wrapper, probing the wrapper would call a user's resolver during registration (and freeze a
DefaultJsonTypeInfoResolver'sModifiers), and inferring from chain shape was shown to misattribute registrations between independent options. So such a copy registers again, like any options whose chain no longer shows the entry.Idea
On .NET 11 the copy constructor also copies
TypeClassifiers, andAddJsonMigrationSupport()adds aJsonMigratableUnionTypeClassifierinstance there. That instance is ours, is added only by registration, and is never shared between independently created options, so it is sound provenance: give it an internal constructor that carries theJsonMigrationTypeInfoResolver, and letMigrationScope.FindCachedlook for it before structural discovery. Copies then keep their registration on .NET 11 regardless of what they do to their chain, while the documented re-registration stays the .NET 10 behavior.Out of scope for #213 because it makes behavior differ by target framework and needs its own documentation.