You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
STJ generator with external (generated) context class #108317
Currently, you cannot stack Roslyn source generators to operate on generated code. As a workaround I opted into running my own generators in a separate project. That is, [JsonSerializable(...)] class MyContext : JsonSerializerContext is generated elsewhere, however this requires the STJ generator to recognize external context classes.
Project1:
// use a custom generator to generate the context, // note this is only a "blueprint" since STJ generator is not yet run.// <auto-genreated/>[JsonSerializable(...)][JsonSerializable(...)]
public class GeneratedContext /* : JsonSerializerContex; */
Project2 references Project1:
// STJ generator won't recognize GeneratedContext in scope// but perhaps we can use something like this to hint on that:[JsonSerializerExternalContext(typeof(GeneratedContext))]// use as attribute sourcepartialclassMyContext:JsonSerializerContex;
This way we can stack generators using separate projects but the workaround only works if the final generator can work with external types.
(PS: this is not a problem specific to STJ though, every time you want to stack generators this can be a blocker, so maybe a more generic solution can be implemented instead)
It's something we had considered, but it didn't quite seem to justify the added complexity. What motivates this use case? Size concerns? Design time performance?
It's a workaround for stacking generators that roslyn doesn't support. The scenario is being able to generate the context itself.
It's something we had considered, but it didn't quite seem to justify the added complexity
Since something like JsonSerializerExternalContext (or maybe MetadataTypeAttribute since the usage is similar) is a direct hint for the generator to know where to look at, I don't think it's necessarily adds too much complexity or perf penalty for this to be considered?
Oh I see, the context on the first project is generated by a custom generator which you want to take into account in the second project. It seems like a niche scenario if I'm honest, it's very unlikely we would be able to prioritize it in the near future.
@eiriktsarpalis If the API is acceptable I can take a stab at it.
The following is roughly the only change needed to cover this:
privatevoidParseJsonSerializerContextAttributes(INamedTypeSymbolcontextClassSymbol,outList<TypeToGenerate>?rootSerializableTypes,outSourceGenerationOptionsSpec?options){Debug.Assert(_knownSymbols.JsonSerializableAttributeType!=null);Debug.Assert(_knownSymbols.JsonSourceGenerationOptionsAttributeType!=null);rootSerializableTypes=null;options=null;foreach(AttributeDataattributeDataincontextClassSymbol.GetAttributes()){INamedTypeSymbol?attributeClass=attributeData.AttributeClass;if(SymbolEqualityComparer.Default.Equals(attributeClass,_knownSymbols.JsonSerializableAttributeType)){/* ... */}elseif(SymbolEqualityComparer.Default.Equals(attributeClass,_knownSymbols.JsonSourceGenerationOptionsAttributeType)){/* ... */}+elseif(SymbolEqualityComparer.Default.Equals(attributeClass,_knownSymbols.MetadataTypeAttributeType))+{+vartypeSymbol=(INamedTypeSymbol?)attributeData.ConstructorArguments[0].Value;+ParseJsonSerializerContextAttributes(typeSymbol,outvarexternalTypes,outvarexternalOptions);+if(externalTypesis not null)+rootSerializableTypes.AddRange(externalTypes);+if(externalOptionsis not null)+options=externalOptions;+}}}
It's not that simple. You need to API propose a new attribute for including contexts. There's also design challenges. What happens if you specify two contexts and they have conflicting contracts for a given type? What happens if you have a ][sonSerializable(typeof(Foo))] in the last context and the transitive closure of either of the earlier contexts also includes a conflicting definition for Foo? This is not an easy problem.
Isn't all that already possible to happen? We're just adding another source for attributes (the "metadata class"), the semantics of the sum of the applied attributes remain intact.
This definitely needs a design review pass but in any case I could start a prototype for that to happen at some point. For one, I'd like this attribute to be sufficient for the generator to find the type so there's practically two entries, JsonSerializableAttribute and MetadataTypeAttribute (or similar).
We don't want to hand-code context for each model, so we created a ApiClientSourceGenerator that automatically generates a ApiClientJsonSerializerContext
But the STJ generator does not see types we have generated and does not generate the code we need
And we can't make the STJ generator work after our generator.
Same issue here - any source generator that generates any sort of DTO might want those types to have source generated serializer contexts.
The general problem is like OP says, source generators (like STJ) cannot reference outputs of other source generators.
I have not dug too deep in the STJ generator yet and probably way oversimplifying, but would it be possible for STJ to expose its serializer context generation as a library method? Then my source generator could generate the same context classes that STJ would have generated, by referencing STJ as a source generator / compile time dependency.
It seems like a niche scenario if I'm honest, it's very unlikely we would be able to prioritize it in the near future. - @eiriktsarpalis
I don't think this is a niche scenario. As mentioned above, a very common scenario is API client generation, e.g. from an OpenAPI description. Perhaps more niche is my intent to eventually support generating API (server) code from OpenAPI.
I don't think this is a niche scenario. As mentioned above, a very common scenario is API client generation, e.g. from an OpenAPI description. Perhaps more niche is my intent to eventually support generating API (server) code from OpenAPI.
Could you share more details? What motivates the use of stacking contexts in that scenario over just doing an E2E generation at the application layer? If the concern is application size I believe the linker will do a good job at trimming away contexts generated at the library layer that are not being used.
We generally recommend doing E2E contract generation at the application layer using the final type graphs. In practice stacking contexts (e.g. as is currently already achievable using TypeInfoResolverChain) creates more problems than it solves, especially when generic types come into play.
This generates all the methods and models needed to interact with the API.
As the developer of the nuget package, it makes sense to want to add those models to a serializer context. As the consumer of the package, this is a very nice feature.
Activity
dotnet-policy-service commentedon Sep 27, 2024
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.
elgonzo commentedon Sep 27, 2024
Have you already tried using
JsonTypeInfoResolver.Combineto combine theMyContext.DefaultandGeneratedContext.Defaultresolvers?alrz commentedon Sep 27, 2024
That doesn't exist, note when you generate the context, STJ generator won't see it. I did elaborate why this is the case in OP.
eiriktsarpalis commentedon Sep 27, 2024
It's something we had considered, but it didn't quite seem to justify the added complexity. What motivates this use case? Size concerns? Design time performance?
alrz commentedon Sep 27, 2024
It's a workaround for stacking generators that roslyn doesn't support. The scenario is being able to generate the context itself.
Since something like
JsonSerializerExternalContext(or maybe MetadataTypeAttribute since the usage is similar) is a direct hint for the generator to know where to look at, I don't think it's necessarily adds too much complexity or perf penalty for this to be considered?eiriktsarpalis commentedon Sep 27, 2024
Oh I see, the context on the first project is generated by a custom generator which you want to take into account in the second project. It seems like a niche scenario if I'm honest, it's very unlikely we would be able to prioritize it in the near future.
alrz commentedon Sep 27, 2024
@eiriktsarpalis If the API is acceptable I can take a stab at it.
The following is roughly the only change needed to cover this:
eiriktsarpalis commentedon Sep 27, 2024
It's not that simple. You need to API propose a new attribute for including contexts. There's also design challenges. What happens if you specify two contexts and they have conflicting contracts for a given type? What happens if you have a
][sonSerializable(typeof(Foo))]in the last context and the transitive closure of either of the earlier contexts also includes a conflicting definition forFoo? This is not an easy problem.alrz commentedon Sep 27, 2024
Isn't all that already possible to happen? We're just adding another source for attributes (the "metadata class"), the semantics of the sum of the applied attributes remain intact.
This definitely needs a design review pass but in any case I could start a prototype for that to happen at some point. For one, I'd like this attribute to be sufficient for the generator to find the type so there's practically two entries, JsonSerializableAttribute and MetadataTypeAttribute (or similar).
sderunov commentedon Mar 17, 2025
We are facing the same problem.
We have an
ApiClientthat contains a large number of subcategories of Api to external services, like this:Each category contains dozens of methods that call an external Api. Example:
We don't want to hand-code context for each model, so we created a
ApiClientSourceGeneratorthat automatically generates aApiClientJsonSerializerContextBut the STJ generator does not see types we have generated and does not generate the code we need
And we can't make the STJ generator work after our generator.
dferretti commentedon Mar 28, 2025
Same issue here - any source generator that generates any sort of DTO might want those types to have source generated serializer contexts.
The general problem is like OP says, source generators (like STJ) cannot reference outputs of other source generators.
I have not dug too deep in the STJ generator yet and probably way oversimplifying, but would it be possible for STJ to expose its serializer context generation as a library method? Then my source generator could generate the same context classes that STJ would have generated, by referencing STJ as a source generator / compile time dependency.
gregsdennis commentedon Mar 29, 2025
I don't think this is a niche scenario. As mentioned above, a very common scenario is API client generation, e.g. from an OpenAPI description. Perhaps more niche is my intent to eventually support generating API (server) code from OpenAPI.
eiriktsarpalis commentedon Mar 29, 2025
Could you share more details? What motivates the use of stacking contexts in that scenario over just doing an E2E generation at the application layer? If the concern is application size I believe the linker will do a good job at trimming away contexts generated at the library layer that are not being used.
We generally recommend doing E2E contract generation at the application layer using the final type graphs. In practice stacking contexts (e.g. as is currently already achievable using
TypeInfoResolverChain) creates more problems than it solves, especially when generic types come into play.gregsdennis commentedon Mar 29, 2025
Consider a nuget package that generates API client code. It provides an attribute that you put on a partial class.
This generates all the methods and models needed to interact with the API.
As the developer of the nuget package, it makes sense to want to add those models to a serializer context. As the consumer of the package, this is a very nice feature.