Skip to content

Migration guide, API renames and read-path fixes - #210

Open
manuc66 wants to merge 8 commits into
feature/split/pr3-newtonsoft-perffrom
feature/split/pr4-migration-fixes
Open

Migration guide, API renames and read-path fixes#210
manuc66 wants to merge 8 commits into
feature/split/pr3-newtonsoft-perffrom
feature/split/pr4-migration-fixes

Conversation

@manuc66

@manuc66 manuc66 commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Problem

  • No migration guide existed for Newtonsoft -> STJ or between the STJ engines.
  • The STJ FallBack* names had inconsistent capitalization.
  • The AOT generator's nested-chain write path duplicated discriminators; the base-as-leaf read path parsed the document twice.

Fix

  • Add MIGRATION.md (Newtonsoft -> STJ and between STJ engines).
  • Rename FallBackSubTypeAttribute / FallBackToNearestAncestor in JsonSubTypes.Text.Json.
  • Fix duplicate discriminators in the AOT generator's nested-chain write path.
  • Eliminate the second JsonDocument parse on the base-as-leaf read path.
  • Prefix benchmark methods per scenario.

Tests

  • Full STJ + AOT + generator suites pass; the golden master is regenerated to match the generator change (net8.0 and net10.0).

Honest note(s)

  • The FallBack* rename is a breaking API change in the experimental STJ package; per the README the public API is not frozen before 1.0.0.

MIGRATION.md is an actionable before/after recipe: the mechanical renames, the
behaviors that actually differ between the Newtonsoft and STJ packages, and the
capability ceilings when moving between the converter, resolver and generator.
Also fix the outdated test count in the STJ status note and link the guide from
the README.
…SubTypes.Text.Json

The STJ package is still a release candidate, so now is the time to fix the
capitalization inherited from the Newtonsoft API: FallBackSubTypeAttribute ->
FallbackSubTypeAttribute and FallBackToNearestAncestor() ->
FallbackToNearestAncestor(). The Newtonsoft package keeps its historical names.

The generator matches attributes by namespace + short name, so it now looks up
FallbackSubTypeAttribute; the native JsonUnknownDerivedTypeHandling enum member
is untouched. All STJ (196), AOT (79) and Newtonsoft (153) tests pass.
…e path

When a nested hierarchy's discriminators are also native properties of the type
(e.g. [JsonPropertyName("$PayloadKind")]), the generated nested-chain writer
emitted every payload property without excluding the discriminator names, so the
injected discriminators were written twice. The runtime converter already
excluded them; the generator now skips any property whose name matches a
discriminator in the chain.

Adds a dedicated test fixture pinning the single-write and the round-trip to the
deepest subtype.
Converter_Serialize/Generated_Deserialize etc. collided across the single-object
and base-as-leaf classes, so a full-suite run produced ambiguous rows. Each class
now uses a unique scenario prefix (Single_, Col_, Nested_, Pres_, Leaf_, Nw_)
and PERFORMANCE.md documents the mapping and how to filter a scenario.
ReadObject parsed the JSON once to resolve the type, then ReadPlainObject parsed
it again from the reader to materialize the base object. Reuse the already-parsed
RootElement instead, matching how the subtype path deserializes.

Measured (BenchmarkDotNet, net10, DefaultJob): Leaf_Converter_Deserialize
1.58us / 560 B before, 1.16us / 360 B after. All STJ and AOT tests pass.
The nested-chain writer now skips properties whose name matches a
discriminator in the chain; the committed PayloadJsonSubTypesConverter
still carried the old output without the guard, failing the golden-master
test.
…ribute-based discriminator

The JsonSubTypeConverterAttribute kept passing a closed converter type to
JsonConverterAttribute, so System.Text.Json built the converter through its
parameterless constructor and the CreateConverter override was never called.
The attribute now leaves ConverterType null for the JsonSubtypes<T> forms and
routes through CreateConverter, which closes the generic over the annotated
type (new [JsonSubTypeConverter("Kind")] convenience constructors) and passes
the discriminator to the converter. As a result the attribute-based write path
now injects the discriminator for registered subtypes, matching the behaviour
the README and MIGRATION.md already documented. GetTypeResolver and
CreateTypeResolver close JsonSubtypes<> over the target type when the attribute
carries no converter type, so the resolver dance keeps working for nested
hierarchies.
Property-presence has no discriminator to inject, so serialization writes
the plain runtime contract. Add a parity test pinning that the runtime
converter and the generated converter emit the same output for a
property-presence subtype (all properties once, nothing injected), and
strengthen the attribute-based runtime test to assert the exact property
set so migrating never loses a property nor duplicates the discriminator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant