-
Notifications
You must be signed in to change notification settings - Fork 10.3k
OpenApi generation creates NullableOf version of structs when null is allowed? #59056
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
Comments
I'm having the same issue. Switching from Swagger generated a bunch of |
I'm also having the same issue. Fun part is that, if a model is using nullable Propriety AND an other model using the same propriety but non nullable this time, it will add it twice in the json as NullableOfProperty & Property |
I encountered the same issue. Here's another problem I noticed:
This happens because metadata like nullable, example, and description are applied to the reference itself rather than being inlined. OpenAPI keywords like oneOf, anyOf, allOf, or not are not used at all. Is there a way to activate this? I came across this blog post about schema reuse, but the enum inlining example doesn't work for me: https://devblogs.microsoft.com/dotnet/dotnet9-openapi/#customize-schema-reuse Swashbuckle:
.NET 9:
|
tl;drYes. A schema that allows The detailed answerSwashbuckle also generates different schemas. The difference is really that Swashbuckle attempts to reuse the "base" (non-nullable) schema in the schema for the nullable type. Unfortunately, the way this is done is not technically correct. To explain why, consider this schema: "MyModel": {
"type": "object",
"properties": {
"myEnum": {
"allOf": [
{
"$ref": "#/components/schemas/MyEnum"
}
],
"description": "Description.",
"nullable": true,
"example": "Value1"
}
}
}, And now let's look at how
By this description, the Aside: the To my knowledge, there is no technically correct way in OpenAPI 3.0.x to reuse the "base" (non-nullable) schema in the schema for the nullable type. But I believe this will be possible with OpenAPI v3.1, which uses "full JSON Schema". In OpenAPI v3.1 you could write: "OptionalAddress": {
"oneOf": [
{
"$ref": "#/definitions/Address"
},
{
"type": "null"
}
]
} There is an open issue for adding OpenAPI v3.1 support in ASP.NET - #58619. Please add a thumb's up reaction on that issue if this is important to you. So what can be done in OpenAPI 3.0.x and .NET 9? One approach could be to avoid generating "nullable" schemas at all. In many cases the "nullability" of a property in a request or response body can be represented as being "optional" -- not in the This is something you can accomplish with a schema transformer that removes builder.Services.AddOpenApi(options =>
{
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (schema.Properties is not null)
{
foreach (var property in schema.Properties)
{
if (schema.Required?.Contains(property.Key) != true)
{
property.Value.Nullable = false;
}
}
}
return Task.CompletedTask;
});
}); Unfortunately this does not handle the case where a nullable property is an enum. I made a couple attempts to fix the enum properties as well but none had the desired outcome. I'll continue to investigate that case but hopefully the above will be helpful to some. |
After a little more investigation and consultation with @captainsafia I now have an improved transformer that handles the NullableOf cases reported here. These arise when value types are used in nullable an non-nullable forms. In addition, nullable enums have The code for this is a little more involved than the transformer I posted in the previous comment, so I will link to an implementation of it in GitHub: I believe this should be a complete solution but pls post here if there are any remaining problems. |
@mikekistler I tried this transformer, and it duplicates enum definitions it seems. One is nullable, another is not: |
@Atulin This could happen if a class or struct used in a request or response body contains an |
@mikekistler It's... actually not |
@Atulin Thank you for reporting this. I've been able to reproduce the problem so now I must figure out why this is happening. |
It looks like the problem comes from public sealed record Command(string Name, string? Description, ETagNamespace? Namespace); Despite being declared as nullable in this record definition, both Description and ETagNamespace are required in the corresponding schema. I would concede that this is not particularly intuitive but the behavior is documented here:
If you search the generated OpenAPI you should find One solution is to declare public sealed record Command(string Name, string? Description) {
public ETagNamespace? Namespace { get; init; }
} You might question the logic of making nullable parameters to the constructor |
I discovered a much simpler fix for the nullable properties in the constructor. If you simply assign them default values like this: public sealed record Command(string Name, string? Description = null, ETagNamespace? Namespace = null); they are no longer included in the But this will generate a There is still the question of whether nullable parameters in the constructor should be But if there are any other problems still lurking on this please raise them here. |
The code snippet you provided @mikekistler fixed this issue at least for me, thanks. Not sure if I should close this or keep it open for a while to see if others have any issues still. I still hope this behavior would be backported to the default implementation, hard to imagine that anyone wants these NullableOf or IEnumerableOfInt64 in their OpenApi documentations.. |
@mikekistler Thanks for the NullableTransformer, but I would keep nullable for nullable non-reference types by adding this: if (Nullable.GetUnderlyingType(context.JsonTypeInfo.Type) != null)
{
schema.Nullable = true;
} Edit: And still, even when the "NullableOf" prefix is gone, it insists of creating two identical enum schemas, but now with postfix "2" for the "nullable". This is very frustrating. |
@jornhd I don't quite understand your comment. Can you give an example of a property where the above logic is needed? |
@mikekistler Sorry, I guess I was overthinking. It's not needed. But it there anyway to get rid of creation of the second type? Consider these request objects/commands: Now this will result in a schema with identical StatusEnum and StatusEnum2! How do I get rid of StatusEnum2 (previously StatusEnum and NullableOfStatusEnum)? When we use nullables in api request objects, we really mean it to be "optional" for the consuming client, so your NullableTransformer is doing the right thing. But as long as it still produces two versions of the same type, it's not ideal. |
@jornhd I think you might be getting two schemas because one has a Regarding:
Only properties with a |
I don't think this issue is resolved and it should be reopened. Yes, the workaround provided by @mikekistler works, but it should not be needed. I don't see a single useful scenario for an extra |
Or |
In general the whole project needs a compatibility layer with Swashbuckle and Nswag. Migrating code from these frameworks causing a lot of friction at the moment. |
Not sure if this is a question, bug report or API proposal, but I was pretty surprised when I tried to switch from Swagger to .NET 9 OpenApi generation, and saw that it generates these NullableOfXXX versions of structs if the API has any endpoints where the struct is nullable in input or output objects. What is the purpose of them?
This makes it harder to migrate from Swagger api generation to this, because we have auto-generated the Typescript schema objects and this would need a lot of changes in the app's TS signatures.
I'm not an OpenApi expert, but couldn't this be handled by using oneOf or anyOf?
Or can this be somehow disabled using the OpenApi configuration? This doesn't happen with classes, why are structs handled differently?
The text was updated successfully, but these errors were encountered: