dotnet / dotnet/runtime

[System.Text.Json] Source generator: accept serializable type requests from other source generators

Open
#133,927 2 comments 1 reaction 0 assignees View on GitHub
area-System.Text.Json untriaged
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

## Summary

This is the circle-back promised in #74841 and the condition #110428 was closed on. Three issues ask the STJ generator to pick up types or contexts produced by another generator, and all three ended at the same wall: generators cannot see each other's output, and the fix belongs in the compiler (dotnet/roslyn#57239). dotnet/roslyn#85239 now proposes a compiler-side mechanism that does not require generator layering or additional compilations, and the Roslyn side has asked for a signal from the teams that would consume it. This issue asks whether the STJ generator would accept type requests from other generators through that mechanism, and what the contract should look like.

Prior discussion:

- #110428, closed as blocked on "the C# compiler supporting source generator layering", with dotnet/roslyn#57239 as the tracking issue.
- #74841, moved to Future "and we can circle back whenever" the Roslyn issue is addressed.
- #108317, open, milestone Future: contexts generated by another generator are invisible to STJ; several commenters describe API client and DTO generation as a common shape of this.
- dotnet/aspnetcore#56021, open, milestone .NET 12: populate the `JsonSerializerContext` automatically with the types minimal API endpoints use.

## What the Roslyn side provides

dotnet/roslyn#85239 adds two methods to `IncrementalGeneratorInitializationContext`. A consumer generator declares a contract type `T` and reads `context.ExternalInputsProvider()` as an ordinary `IncrementalValuesProvider`; any producer generator publishes values with `context.RegisterExternalOutput(provider)`. Values are computed by the producer from the input compilation, so a producer that reacts to attributes on user types can participate. Everything runs against the same compilation every generator sees today; there is no new phase and no new `Compilation`. Generators that do not use it are not affected.

The contract is a plain data record owned by the consumer, shipped as a small `netstandard2.0` package so producers can compile against it. The driver serializes values across the analyzer load context boundary, so the contract must stay plain data.

## What STJ would need to do

Define the contract:

```csharp
namespace System.Text.Json.SourceGeneration
{
// TypeName is a fully qualified metadata name.
// ContextName is the metadata name of the JsonSerializerContext that should own the type.
public sealed record SerializableTypeRequest(string TypeName, string ContextName);
}
```

Consume it in the generator, next to the existing `[JsonSerializable]` pipeline:

```csharp
var requested = context
.ExternalInputsProvider()
.Combine(context.CompilationProvider)
.Select(static (pair, ct) => ResolveRequest(pair.Left, pair.Right, ct));
```

A request is resolved with `GetTypeByMetadataName` and then handled exactly like one more `[JsonSerializable(typeof(T))]` on the named context: same root collection, same transitive closure, same `DuplicateTypeName` rule when two roots collide, same `TypeNotSupported` diagnostic when the type cannot be generated. An unresolved name produces a diagnostic rather than a silent skip. The internal spec types are not part of the contract and stay free to change.

Two design points I would rather have this team decide than assume:

- Whether a request may name a context that user code has not declared. If STJ creates it, the producer can reference the context from its own generated code; if not, the producer's users declare a partial context first, as they do today.
- Whether the request should carry `TypeInfoPropertyName` and `GenerationMode`, or start with the two fields above and grow additively if needed.

On conflicts, which was the concern raised in #108317: requests do not introduce cross-context delegation. A request adds a root to one named context, and the conflict rules are the ones that already apply to two attributes on the same context. Stacking contexts across assemblies (#124889) is a separate question and is not changed by this.

## Who would produce requests

- ASP.NET's Request Delegate Generator already knows every parameter and response type of every minimal API endpoint. Publishing them closes dotnet/aspnetcore#56021 without user annotations. I am opening the matching issue in dotnet/aspnetcore.
- Endpoint frameworks that generate their own `Map` calls, which is where I come from. FastEndpoints today ships a separate CLI tool that writes serializer contexts to disk for users to check in, citing the chaining limitation as the reason.
- API client generators that emit DTOs from an OpenAPI document (the #108317 shape). With `RegisterPreCompilationSourceOutput` (dotnet/roslyn#83089) the DTOs land in the input compilation because they come from an additional file, and a request for each of them gives the serializer context. Without it, that scenario stays out of reach.

## What this does not cover

A request must name a type in the input compilation: declared by the user, from a referenced assembly, or emitted through pre-compilation output. Types or members emitted by a generator in the standard phase are still invisible, so the Lombok-style case in #74841 is not addressed by this. That is a deliberate limit on the Roslyn side to avoid a second compilation.

## The ask

Would the STJ generator take a consumer branch of this shape if dotnet/roslyn#85239 is approved? A yes or no here is what the Roslyn discussion needs to move; the contract details can follow.

cc @eiriktsarpalis

Contributor guide

Open the contributing guide

Research direction

Start with the existing [JsonSerializable] pipeline in the System.Text.Json source generator and read Roslyn issues #85239 and #57239 to understand the proposed external-input contract. Review the unresolved choices about undeclared contexts and request fields. Done means the team has decided whether to consume the mechanism and documented the contract and behavior for requests, conflicts, and unresolved types.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.