Skip to content

feat: catch a type missing from the JsonSerializerContext (RXH024) - #7

Merged
Katyara11 merged 1 commit into
mainfrom
feat/json-context-coverage
Nov 7, 2026
Merged

Katyara11 merged 1 commit into
mainfrom
feat/json-context-coverage

Conversation

@Katyara11

@Katyara11 Katyara11 commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes the last sharp edge in the AOT story: a type your contracts serialize that your
JsonSerializerContext does not register is now RXH024 at build time, instead of a resolver
error on the first call that needs it in a deployed trimmed application.

The generator already knows the complete type set, so you no longer keep the context in step by
hand.

Changes

  • RXH024 (warning) — reports a serialized type no JsonSerializerContext registers. The
    message names the type, the method, the exact [JsonSerializable] line, and the context.
  • src/Runax.Http.SourceGenerator/JsonContextCoverage.cs — the type collection, the context scan
    and the comparison, plus the recorded reason the context cannot simply be emitted.
  • A second, compilation-wide pipeline in ClientGenerator — the check joins every contract's types
    against every context, which no per-interface analysis can see.
  • 16 tests; docs in aot.md, serialization.md, diagnostics.md, package README, CHANGELOG.

Type

  • Feature
  • Bug fix
  • Refactor
  • Chore (CI, deps, config)

Checklist

  • Code builds without errors
  • Tests added/updated

Notes for review

Why this is a diagnostic and not a generated context

I verified the constraint instead of assuming it, and the answer is more precise than "generators
cannot see each other's output":

Registration Visible to System.Text.Json's generator?
RegisterPostInitializationOutput Yes — compiles, populated, serializes at run time
RegisterSourceOutput (derived from user code) No — CS0534, abstract members unimplemented

Post-initialization output joins the compilation every generator sees; analysis-derived output does
not. The type set can only come from analysing the contracts, which means RegisterSourceOutput —
so emitting the context is impossible, not merely awkward. Recorded in docs/aot.md and on
JsonContextCoverage so nobody retries it.

Two deliberate blind spots

A diagnostic that cries wolf gets suppressed wholesale and then protects nobody, so both of these
err toward silence:

  • Bare primitives and string are never reported. A probe showed a context registering a
    single record resolved its int and string property types for free, while bool and every
    collection came back null. So nothing is unconditionally free — but those are overwhelmingly
    likely to be, and a method whose body or response is a bare primitive is rare.
  • Any context counts. Nothing at compile time can tell which context is wired to which client,
    and several may be. Taking the union is the only choice that cannot invent a false positive.

Warning, not error

A mixed setup — one client on a source-generated context, another deliberately on the
reflection-based serializer — is legitimate, and an error would fail that build with no recourse
but suppression. Under warnings-as-errors it fails the build anyway, which is how the AOT smoke
test guards its own context: deleting a [JsonSerializable] from AotJsonContext now fails the
build, which I confirmed before restoring it.

Collections are the gap worth catching

Registering Repository does not cover IReadOnlyList<Repository> — confirmed by probe. A
method returning Task<IReadOnlyList<T>> is exactly where someone forgets, and it is covered.

Implementation note

The type set is read off the existing ClientModel rather than re-walking symbols, so the
generator keeps one understanding of what each return shape serializes. A second walk would drift
the first time a return shape is added. ClientGenerator edits are confined to Initialize,
AnalysisResult and one return statement, to keep the rebase against the parallel query-binding PR
mechanical.

Verification

1527 tests across net8.0/net9.0/net10.0; --warnaserror clean, dotnet format clean, AOT publish
with zero trim warnings and the binary runs. All doc links and anchors resolve.

Rebased onto main twice while open, since #5 and #6 landed first. #6 claimed RXH022 and
RXH023, so this diagnostic is RXH024 — ids are never reused, so the renumber rather than a
shared number is the right resolution. The two generator changes touch the same files but not the
same code paths, and the combined suite is green.

@Katyara11
Katyara11 force-pushed the feat/json-context-coverage branch from 232cfc2 to eae94fb Compare November 7, 2026 11:26
Closes the last sharp edge in the AOT story. The generator already knows every
type a contract sends and receives, so the user should not be the one tracking
whether their JsonSerializerContext kept up. A missing registration is now a
build warning naming the type, the method, the exact [JsonSerializable] line
and the context to add it to — instead of a resolver error on the first call
that needed it, in a deployed trimmed application, a long way from the
registration nobody updated.

The context itself cannot be generated, and I verified why rather than
assuming. Roslyn output registered with RegisterPostInitializationOutput joins
the compilation every generator sees: a JsonSerializerContext emitted that way
IS picked up and populated by System.Text.Json's generator, and serializes at
run time. Output registered with RegisterSourceOutput is not — the other
generator never sees it and the emitted partial fails CS0534 for the abstract
members nobody implemented. The type set can only come from analysing the
contracts, which means RegisterSourceOutput, so emission is impossible rather
than merely awkward. Recorded in docs/aot.md and on JsonContextCoverage so the
next person does not retry it.

Covers [Body] parameters that reach the serializer, Task<T>,
Task<ApiResponse<T>> and every [Response] type — including the collection type
for Task<IReadOnlyList<T>>, since a probe confirmed registering T does not
cover IReadOnlyList<T>. That is the easiest gap to miss by hand and the one
most worth catching.

Two blind spots, both chosen to avoid warning when nothing is wrong, because a
diagnostic that cries wolf gets suppressed wholesale and then protects nobody.
Bare primitives and string are never reported: a probe showed a context
registering one record resolved its int and string property types for free,
while bool and every collection came back null — so nothing is unconditionally
free, but those are overwhelmingly likely to be. And a type registered by any
context counts, since nothing at compile time can tell which context is wired
to which client.

A warning rather than an error: a mixed setup, one client on a source-generated
context and another deliberately on the reflection-based serializer, is
legitimate and an error would fail that build with no recourse but suppression.
Under warnings-as-errors — this repository included — it fails the build
anyway, which is how the AOT smoke test now guards its own context.

Reads the type set off the existing model rather than re-walking symbols, so
the generator keeps exactly one understanding of what each return shape
serializes; a second walk would drift the first time a shape is added.

1416 tests; build clean with warnings-as-errors, format clean, AOT publishes
with zero trim warnings and runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Katyara11
Katyara11 force-pushed the feat/json-context-coverage branch from eae94fb to c439da6 Compare November 7, 2026 11:37
@Katyara11 Katyara11 changed the title feat: catch a type missing from the JsonSerializerContext (RXH022) feat: catch a type missing from the JsonSerializerContext (RXH024) Nov 7, 2026
@Katyara11
Katyara11 merged commit 5c08a43 into main Nov 7, 2026
2 checks passed
@Katyara11
Katyara11 deleted the feat/json-context-coverage branch November 7, 2026 12:13
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