-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support exporting STJ serialization contracts to JSON schema #100159
Comments
Is the label-tagger bot broken? |
It was having issues a few weeks back. |
How does this interact with discriminated unions (e.g. |
@benlongo after a bit of experimentation I ended up using |
Hi @eiriktsarpalis, thanks for looking into this! As a preface, I'm not very familiar with the intricacies of JSON Schema, so take anything I say with a grain of salt :) Regarding the in-progress OpenAPI work (which I've left a related comment on dotnet/aspnetcore#54598 (comment)), I'm concerned that the difference between JSON Schema and OpenAPI will cause paper cuts around discriminated unions; if the OpenAPI implementation is to naively delegate schema generation for discriminated unions, then things won't work properly. We use a lot of discriminated unions in our data model so I'm very invested in it working properly. I'll use the example objects (modified slightly) from the OpenAPI 3.1.0 spec (https://swagger.io/specification/#discriminator-object). This translates to the following STJ model. [ JsonPolymorphic( TypeDiscriminatorPropertyName = "petType" ) ]
[ JsonDerivedType( typeof(Cat), Cat.PetType ) ]
[ JsonDerivedType( typeof(Dog), Dog.PetType ) ]
[ JsonDerivedType( typeof(Lizard), Lizard.PetType ) ]
public abstract record Animal;
public record Cat : Animal {
public const string PetType = "cat";
public required string Name { get; init; }
}
public record Dog : Animal {
public const string PetType = "dog";
public required string Bark { get; init; }
}
public record Lizard : Animal {
public const string PetType = "lizard";
public required bool LovesRocks { get; init; }
} In JSON Schema world, I would expect this to get mapped to something very similar to what you have in your prototype: an In OpenAPI world however, discriminated unions are handled differently. I would expect the following OpenAPI schema to be generated for Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Lizard'
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
lizard: '#/components/schemas/Lizard' I don't think that the STJ JSON Schema library should be aware of OpenAPI peculiarities, but I definitely think the proper escape hatches need to exist so that the OpenAPI implementation can generate the correct schema. I have no idea what those escape hatches look like, or if they already exist, but I can imagine how a simple implementation of OpenAPI would result in this being difficult or impossible to express. The OpenAPI implementation will have to be aware of the underlying contract somehow to bypass JSON Schema generation for certain cases like this. As an aside, based on https://json-schema.org/understanding-json-schema/reference/combining it seems like it could make more sense to use
One place I could |
I agree with that sentiment, it's something we've been looking at solving with @captainsafia. The prototype uses a callback API that lets users append or modify JSON schema documents based on presence of particular properties, although this particular use case makes things trickier.
The problem with |
For some additional context, OpenAPI 3.1 is built on JSON Schema 2020-12 by default. Even previous versions of OpenAPI use a modified JSON Schema draft 4. Discriminated unions aren't a problem that JSON Schema has. They're a problem that C# has. The |
For the fully general case, I'm also not sure how one would define numeric discriminators in the OpenAPI
Thanks for this context there. I took a quick skim through
Just so I understand what you're getting at here, my understanding is that serializing JSON discriminated unions used to be a problem for C# (particularly STJ), but is no longer an issue with
Thanks for explaining the annotation behavior - I was not aware of that. If I'm understanding this correctly, the OpenAPI additions ( It sounds like you are describing one possible use of the OpenAPI document at runtime where there is a module validating based on JSON Schema, and then another module adding OpenAPI information onto these results (correct me if I'm wrong there). The only use case I have experience with is client code generation from the OpenAPI document. In this scenario, the JSON Schema may not be used for validation (directly at least), but rather for typing/parsing information. I don't believe that exact flow will always be taken, but the annotations being structurally valid JSON Schema is definitely relevant. It seems like the addition of
Do these callbacks expose strictly JSON Schema information, or can STJ contract data be accessed through this interface? Another possibly relevant issue (not really a bug per se) that I've run into with STJ is that sub-types of a |
Draft 5 is basically the OpenAPI-specific draft 4 variant. Draft 5 is basically never supported outside of OpenAPI.
What I mean is that c# doesn't support unions, as such. Using
Importantly, JSON Schema isn't a typing system. It's a constraints system. Henry Andrews' excellent blog post explains this difference well. And code generation (either direction) isn't defined by any specification (yet), so whoever implements it is free to do what they want. |
We could try to make the generator a bit more clever and emit |
There is an important distinction between |
Background and motivation
The recent popularity of function calling capabilities in LLMs as well as the upcoming OpenAPI work in ASP.NET Core has highlighted the importance of a System.Text.Json component that is capable of exporting its own serialization contracts (
JsonTypeInfo
) to JSON schema documents. Such a component should ideally satisfy the following criteria:JsonSerializerOptions
and POCO attribute annotations (e.g.JsonNamingPolicy
,JsonNumberHandling
,JsonPropertyName
,JsonIgnore
, etc.)I wrote a prototype that attempts to address the above design goals, and this was largely achieved by tapping into the metadata exposed by the STJ contract model. That being said, the existing contract APIs do not expose all metadata that is necessary to construct a schema, so in many cases the implementation had to resort to private reflection or outright replication of STJ internals. At the same time, the core mapping logic itself requires acute understanding of STJ esoterica, so it cannot be expected that such a component could be sustainably maintained by third-party authors.
I'm creating this issue to track .NET 9 work related to JSON schema extraction. The scope is related to and overlaps with #29887 but doesn't necessarily coincide with it. At a high level, it is tracking the following goals (in order of importance):
JsonTypeInfo
contracts to JSON schema documents. Most users should able to use that directly, but would also serve as a reference implementation for those that want to map to bespoke formats (e.g. OpenAPI YAML).JsonSchema
exchange type. This is a stretch goal for .NET 9 since it would likely necessitate implementing support for the full JSON schema specification (whereas a mapper need only target a subset of the spec).Work Items
JsonObject
should support property order manipulation #102932Expose enum type contract metadata (int vs string serialization, custom naming policies, etc.)ExposeNullable<T>
contract metadata (element type, custom element converters, etc.)JsonPropertyInfo.AttributeProvider
metadata. #100095JSON schema exchange type.(Cut for .NET 9)The text was updated successfully, but these errors were encountered: