AddJsonMigrationSupport() registers JsonMigratableConverterFactory in options.Converters. Any entry in that list disables System.Text.Json's source-generated fast-path serialization for every type in the options, including types that never touch migration.
Verified in STJ source (release/10.0 and main):
JsonSerializerContext.IsCompatibleWithOptions requires options.Converters.Count == 0 (plus option equality with the generated options).
JsonTypeInfo.Configure sets CanUseSerializeHandler = HasSerializeHandler && IsCompatibleWithCurrentOptions; when Options.CanUseFastPathSerializationLogic is false the per-node check falls back to OriginatingResolver.IsCompatibleWithOptions(Options), which for a context is the same Converters.Count == 0 check.
- Nested nodes take the fast path through
JsonMetadataServicesConverter.OnTryWrite whenever their own node is compatible, so the fix is per-type, not all-or-nothing.
- Modifying a type info's property list sets
IsCustomized = true (via VerifyMutable), so the injected $type property correctly keeps the migratable type itself on the metadata path.
TypeClassifiers (.NET 11) are not part of the compatibility check.
The benchmarks show it: the source-gen "JsonMigratable" serialize numbers equal the reflection numbers.
| Size |
Plain SG |
Migratable SG |
Plain refl |
Migratable refl |
| Small |
93 ns |
212 ns |
129 ns |
205 ns |
| Medium |
541 ns |
948 ns |
770 ns |
863 ns |
| Large |
4,169 ns |
6,537 ns |
6,331 ns |
6,313 ns |
Change
Register migration through the type-info resolver chain instead of options.Converters:
Also fix the benchmark artifact while there: the perf types use bare [JsonMigratable], so $type carries the 65-char full type name and the "2.43x alloc ratio" on Small serialize is just the larger output byte[] (56 B → 136 B). Give the perf types short TypeDiscriminator values like the polymorphic guardrail already does, so the tables measure library cost rather than payload size.
AddJsonMigrationSupport()registersJsonMigratableConverterFactoryinoptions.Converters. Any entry in that list disables System.Text.Json's source-generated fast-path serialization for every type in the options, including types that never touch migration.Verified in STJ source (release/10.0 and main):
JsonSerializerContext.IsCompatibleWithOptionsrequiresoptions.Converters.Count == 0(plus option equality with the generated options).JsonTypeInfo.ConfiguresetsCanUseSerializeHandler = HasSerializeHandler && IsCompatibleWithCurrentOptions; whenOptions.CanUseFastPathSerializationLogicis false the per-node check falls back toOriginatingResolver.IsCompatibleWithOptions(Options), which for a context is the sameConverters.Count == 0check.JsonMetadataServicesConverter.OnTryWritewhenever their own node is compatible, so the fix is per-type, not all-or-nothing.IsCustomized = true(viaVerifyMutable), so the injected$typeproperty correctly keeps the migratable type itself on the metadata path.TypeClassifiers(.NET 11) are not part of the compatibility check.The benchmarks show it: the source-gen "JsonMigratable" serialize numbers equal the reflection numbers.
Change
Register migration through the type-info resolver chain instead of
options.Converters:IJsonTypeInfoResolverthat returnsJsonMetadataServices.CreateValueInfo<T>(options, converter)for[JsonMigratable]types andnullfor everything else, and insert it at index 0 ofoptions.TypeInfoResolverChaininAddJsonMigrationSupport().JsonMigratableConverterFactory.CreateConverterCore, the exclusion clone swaps the resolver for a type-excluding one (same index) instead of swapping the converter factory.JsonMigratableTypes.HasConverterOverride(it currently distinguishes the factory from user converters viaoptions.Converters) andJsonMigratableUnionTypeClassifier.CreateJsonClassifier(it locates the registry by scanningoptions.Converters).DefaultJsonTypeInfoResolveron freeze, so the migration resolver must fall back to a reflection resolver when it is the only entry (or the library appends one). Do not shadow a context added later.docs/recipes/aot-source-gen.mdadd the context afterAddJsonMigrationSupport(); make that order work or document the new order.JsonSerializerOptionsCombinations*Testsshould cover: unrelated type keepsSerializeHandlerfast path (observable throughJsonTypeInfo<T>.SerializeHandlerplus a serialize round trip), nested[JsonMigratable]property inside a plain type still writes$type, converters registered by the user before and afterAddJsonMigrationSupport()keep today's precedence.docs/perf/*.mdand the README perf table. Expected: unrelated and nested plain types (PerfPayload) regain the fast path; Large serialize should move from ~6.5 µs toward the ~4.4 µs plain number. The migratable root type stays on the metadata path until its discriminator is a real member (follow-up in the generator issue).Also fix the benchmark artifact while there: the perf types use bare
[JsonMigratable], so$typecarries the 65-char full type name and the "2.43x alloc ratio" on Small serialize is just the larger outputbyte[](56 B → 136 B). Give the perf types shortTypeDiscriminatorvalues like the polymorphic guardrail already does, so the tables measure library cost rather than payload size.