Closed feliceiorillo closed 1 year ago
As per the JSON Schema specification, you can’t assign additional properties on a “reference” schema. That is, if a schema instance uses “$ref”, then it cannot have other properties assigned, including “nullable”. So, you have to apply “UseAllOfToExtendReferenceSchemas” to do this while maintaining a valid Open API / Swagger definition. You should look into what that’s breaking for you because it shouldn’t.
Thanks for your reply! I understand what you mean. But, is there any possibility, even changing the code of this nuget, to achieve my goal? I know this would be against the JSON Schema specification, but I think it's possible. Can you show me where is the code to change to do that?
I have the same issue using NewtonSoft. I tried to make an unittest in a clone of this repository to reproduce the error but I couldn't reproduce this. I use dotnet core 3.1 with version 6.3 of Swashbuckle.AspNetCore. As a work-around is added a MapType<> for the enum that is used in the nullable property.
I have the same issue using NewtonSoft. I tried to make an unittest in a clone of this repository to reproduce the error but I couldn't reproduce this. I use dotnet core 3.1 with version 6.3 of Swashbuckle.AspNetCore. As a work-around is added a MapType<> for the enum that is used in the nullable property.
Hi, thanks for your suggestion. I have something like 30 different enums, not so suitable solution in my case...
I committed the unittest (https://github.com/anoordover/Swashbuckle.AspNetCore/commit/c11f3bcbcdcd319931183a839149d7c2b7b54d3f)... Oeps... committed my experiment. Now with the correct code using a nullable enum: https://github.com/anoordover/Swashbuckle.AspNetCore/commit/d4d51c9cd10f3761f4b7490729b9049fff69d7b4 B.t.w. the unittest fails on my PC
Maybe there is a work-around. Debugging my tests stopped on line 224 in SchemaGenerator. On that line a property UseInlineDefinitionsForEnums is used to decide to return the definition for the enum inline or as a ref. I will try this in my own solution and let you know.
Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); }
But still... on line 224 the definition should be inline when a property is nullable.
I think this is a complex fix... A DataContract gets created based on the type of the property. This DataContract object doesn't seem to contain information about "nullable" of the property.
Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); }
But still... on line 224 the definition should be inline when a property is nullable.
Adding UseAllOfToExtendReferenceSchemas() instead of UseInlineDefinitionsForEnums() also works as a work-around...
UseAllOfToExtendReferenceSchemas breaks my client generation...
Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); } But still... on line 224 the definition should be inline when a property is nullable.
Adding UseAllOfToExtendReferenceSchemas() instead of UseInlineDefinitionsForEnums() also works as a work-around...
@feliceiorillo did you also try UseInlineDefinitionsForEnums()?
Closing as the solution (i.e. to use UseAllOfToExtendReferenceSchemas
) has already been provided. If that's breaking client generators then you should create an issue with them instead of SB.
You could also try UseInlineDefinitionsForEnums
as suggested by @anoordover
Solution that worked for me.
/// <summary>
/// Updates already generated schema by marking $ref enum properties in it as nullable
/// if the orginal class property is of type nullable enum type.
/// NB: schema filter can modify Schemas after they're initially generated.
/// </summary>
public class NullableEnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var isReferenceType =
TypeHelper.IsReference(context.Type) &&
!TypeHelper.IsCLR(context.Type) &&
!TypeHelper.IsMicrosoft(context.Type);
if(!isReferenceType) { return; }
var bindingFlags = BindingFlags.Public | BindingFlags.Instance;
var members = context.Type.GetFields(bindingFlags).Cast<MemberInfo>()
.Concat(context.Type.GetProperties(bindingFlags))
.ToArray();
var hasNullableEnumMembers = members.Any(x => TypeHelper.IsNullableEnum(x.GetMemberType()));
if (!hasNullableEnumMembers) { return; }
schema.Properties.Where(x => !x.Value.Nullable).ForEach(property =>
{
var name = property.Key;
var possibleNames = new string[]
{
name,
TextCaseHelper.ToPascalCase(name),
}; // handle different cases
var sourceMember = possibleNames
.Select(n => context.Type.GetMember(n, bindingFlags).FirstOrDefault())
.Where(x => x != null)
.FirstOrDefault();
if (sourceMember == null) { return; }
var sourceMemberType = sourceMember.GetMemberType();
if (sourceMemberType == null || !TypeHelper.IsNullableEnum(sourceMemberType)) { return; }
// manual nullability fixes
if (property.Value.Reference != null)
{
// option 1 - OpenAPI 3.1
// https://stackoverflow.com/a/48114924/5168794
//property.Value.AnyOf = new List<OpenApiSchema>()
//{
// new OpenApiSchema
// {
// Type = "null",
// },
// new OpenApiSchema
// {
// Reference = property.Value.Reference,
// },
//};
// property.Value.Reference = null;
// option 2 - OpenAPI 3.0
// https://stackoverflow.com/a/48114924/5168794
property.Value.Nullable = true;
property.Value.AllOf = new List<OpenApiSchema>()
{
new OpenApiSchema
{
Reference = property.Value.Reference,
},
};
property.Value.Reference = null;
// option 3 - OpenAPI 3.0
// https://stackoverflow.com/a/23737104/5168794
//property.Value.OneOf = new List<OpenApiSchema>()
//{
// new OpenApiSchema
// {
// Type = "null",
// },
// new OpenApiSchema
// {
// Reference = property.Value.Reference,
// },
//};
//property.Value.Reference = null;
}
});
}
}
public static class TypeHelper
{
/// <summary>
/// Checks if type is CLR type.
/// </summary>
public static bool IsCLR(Type type) => type.Assembly == typeof(int).Assembly;
/// <summary>
/// Checks if type is Microsoft type.
/// </summary>
public static bool IsMicrosoft(Type type) => type.Assembly.FullName?.StartsWith("Microsoft") ?? false;
/// <summary>
/// Checks if type is value type.
/// </summary>
public static bool IsValue(Type type) => type.IsValueType;
/// <summary>
/// Checks if type is reference type.
/// </summary>
public static bool IsReference(Type type) => !type.IsValueType && type.IsClass;
/// <summary>
/// Checks if property type is nullable reference type.
/// NB: Reflection APIs for nullability information are available from .NET 6 Preview 7.
/// </summary>
public static bool IsNullableReferenceProperty(PropertyInfo property) =>
new NullabilityInfoContext().Create(property).WriteState is NullabilityState.Nullable;
/// <summary>
/// Checks if type is enum type.
/// </summary>
public static bool IsEnum(Type type) => type.IsEnum || (Nullable.GetUnderlyingType(type)?.IsEnum ?? false);
/// <summary>
/// Checks if type is nullable enum type.
/// </summary>
public static bool IsNullableEnum(Type type) => Nullable.GetUnderlyingType(type)?.IsEnum ?? false;
/// <summary>
/// Checks if type is not nullable enum type.
/// </summary>
public static bool IsNotNullableEnum(Type type) => IsEnum(type) && !IsNullableEnum(type);
}
Based on:
I am running .NET 6.0 ASP.Net Core Web Api with SwashBuckle.AspNetCore version 6.3.0 (currently last stable)
The swagger.json doesn't show nullable: true where there is a nullable enum in a dto class
{ "openapi": "3.0.1", "info": { "title": "TestBug", "version": "1.0" }, "paths": { "/api/My": { "get": { "tags": [ "My" ], "responses": { "200": { "description": "Success", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/MyDTO" } }, "application/json": { "schema": { "$ref": "#/components/schemas/MyDTO" } }, "text/json": { "schema": { "$ref": "#/components/schemas/MyDTO" } } } } } } } }, "components": { "schemas": { "MyDTO": { "type": "object", "properties": { "myEnum": { "$ref": "#/components/schemas/MyEnum" }, "myNullableEnum": { "$ref": "#/components/schemas/MyNullableEnum" }, "myNullableInt": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "MyEnum": { "enum": [ 0, 1, 2 ], "type": "integer", "format": "int32" }, "MyNullableEnum": { "enum": [ 0, 1, 2 ], "type": "integer", "format": "int32" } } } }
Steps to reproduce:
I am aware of using "UseAllOfToExtendReferenceSchemas", but it's not suitable in my case, since it's breaking other logics.