Skip to content

perf(stjm): register migration through the resolver chain so source-gen fast-path serialization survives #207

Description

@egil

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:

  • Add a migration IJsonTypeInfoResolver that returns JsonMetadataServices.CreateValueInfo<T>(options, converter) for [JsonMigratable] types and null for everything else, and insert it at index 0 of options.TypeInfoResolverChain in AddJsonMigrationSupport().
  • In JsonMigratableConverterFactory.CreateConverterCore, the exclusion clone swaps the resolver for a type-excluding one (same index) instead of swapping the converter factory.
  • Adapt JsonMigratableTypes.HasConverterOverride (it currently distinguishes the factory from user converters via options.Converters) and JsonMigratableUnionTypeClassifier.CreateJsonClassifier (it locates the registry by scanning options.Converters).
  • Reflection users: when the chain is empty at registration time STJ will no longer populate DefaultJsonTypeInfoResolver on 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.
  • Registration order: the README and docs/recipes/aot-source-gen.md add the context after AddJsonMigrationSupport(); make that order work or document the new order.
  • JsonSerializerOptionsCombinations*Tests should cover: unrelated type keeps SerializeHandler fast path (observable through JsonTypeInfo<T>.SerializeHandler plus a serialize round trip), nested [JsonMigratable] property inside a plain type still writes $type, converters registered by the user before and after AddJsonMigrationSupport() keep today's precedence.
  • Re-run benchmarks and refresh docs/perf/*.md and 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 $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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requeststjmEgil.SystemTextJson.Migration scope

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions