Skip to content

Commit

Permalink
Support non-nullable types as required (#2803)
Browse files Browse the repository at this point in the history
Add support for configuring non-nullable types as `required: true`.

---------

Co-authored-by: blouflashdb <[email protected]>
Co-authored-by: Simon Rask <[email protected]>
  • Loading branch information
3 people authored Jul 23, 2024
1 parent 3553751 commit 85fefe4
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions targ
target.DiscriminatorValueSelector = source.DiscriminatorValueSelector;
target.UseAllOfToExtendReferenceSchemas = source.UseAllOfToExtendReferenceSchemas;
target.SupportNonNullableReferenceTypes = source.SupportNonNullableReferenceTypes;
target.NonNullableReferenceTypesAsRequired = source.NonNullableReferenceTypesAsRequired;
target.SchemaFilters = new List<ISchemaFilter>(source.SchemaFilters);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ public static void SupportNonNullableReferenceTypes(this SwaggerGenOptions swagg
swaggerGenOptions.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true;
}

/// <summary>
/// Enable detection of non nullable reference types to mark the member as required in schema properties
/// </summary>
/// <param name="swaggerGenOptions"></param>
public static void NonNullableReferenceTypesAsRequired(this SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SchemaGeneratorOptions.NonNullableReferenceTypesAsRequired = true;
}

/// <summary>
/// Automatically infer security schemes from authentication/authorization state in ASP.NET Core.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,26 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Options;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class SchemaGenerator : ISchemaGenerator
{
private readonly SchemaGeneratorOptions _generatorOptions;
private readonly ISerializerDataContractResolver _serializerDataContractResolver;
private readonly IOptions<MvcOptions> _mvcOptions;

public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataContractResolver serializerDataContractResolver)
: this(generatorOptions, serializerDataContractResolver, null)
{
}

public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataContractResolver serializerDataContractResolver, IOptions<MvcOptions> mvcOptions)
{
_generatorOptions = generatorOptions;
_serializerDataContractResolver = serializerDataContractResolver;
_mvcOptions = mvcOptions;
}

public OpenApiSchema GenerateSchema(
Expand Down Expand Up @@ -410,8 +418,15 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi
? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty)
: GenerateSchemaForType(dataProperty.MemberType, schemaRepository);

var markNonNullableTypeAsRequired = _generatorOptions.NonNullableReferenceTypesAsRequired
#if !NETSTANDARD2_0
&& (!_mvcOptions?.Value.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes ?? true)
#endif
&& (dataProperty.MemberInfo?.IsNonNullableReferenceType() ?? false);

if ((
dataProperty.IsRequired
|| markNonNullableTypeAsRequired
|| customAttributes.OfType<RequiredAttribute>().Any()
#if NET7_0_OR_GREATER
|| customAttributes.OfType<System.Runtime.CompilerServices.RequiredMemberAttribute>().Any()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public SchemaGeneratorOptions()

public bool SupportNonNullableReferenceTypes { get; set; }

public bool NonNullableReferenceTypesAsRequired { get; set; }

public IList<ISchemaFilter> SchemaFilters { get; set; }

private string DefaultSchemaIdSelector(Type modelType)
Expand Down Expand Up @@ -67,4 +69,4 @@ private string DefaultDiscriminatorValueSelector(Type subType)
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using Xunit;

Expand Down Expand Up @@ -75,7 +77,7 @@ public void Apply_EnrichesResponseMetadata_IfActionDecoratedWithSwaggerResponseC
.GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerResponseContentTypesAttributes));
var filterContext = new OperationFilterContext(
apiDescription: null,
schemaRegistry: new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions())),
schemaRegistry: new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions()), Options.Create<MvcOptions>(new MvcOptions())),
schemaRepository: new SchemaRepository(),
methodInfo: methodInfo);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -866,7 +867,7 @@ private static SchemaGenerator Subject(
var serializerSettings = new JsonSerializerSettings();
configureSerializer?.Invoke(serializerSettings);

return new SchemaGenerator(generatorOptions, new NewtonsoftDataContractResolver(serializerSettings));
return new SchemaGenerator(generatorOptions, new NewtonsoftDataContractResolver(serializerSettings), Options.Create<MvcOptions>(new MvcOptions()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static void DeepCopy_Copies_All_Properties()

// If this assertion fails, it means that a new property has been added
// to SwaggerGeneratorOptions and ConfigureSchemaGeneratorOptions.DeepCopy() needs to be updated
Assert.Equal(12, publicProperties.Length);
Assert.Equal(13, publicProperties.Length);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;
Expand Down Expand Up @@ -738,6 +739,68 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypesInDict
Assert.Equal(expectedNullable, propertySchema.Nullable);
}

[Theory]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNullableContent), nameof(TypeWithNullableContext.NullableString), false)]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), true)]
public void GenerateSchema_SupportsOption_NonNullableReferenceTypesAsRequired_RequiredAttribute_Compiler_Optimizations_Situations(
Type declaringType,
string subType,
string propertyName,
bool required)
{
var subject = Subject(
configureGenerator: c => c.NonNullableReferenceTypesAsRequired = true
);
var schemaRepository = new SchemaRepository();

subject.GenerateSchema(declaringType, schemaRepository);

var propertyIsRequired = schemaRepository.Schemas[subType].Required.Contains(propertyName);
Assert.Equal(required, propertyIsRequired);
}

[Theory]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), false)]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), true)]
public void GenerateSchema_SupportsOption_SuppressImplicitRequiredAttributeForNonNullableReferenceTypes(
Type declaringType,
string subType,
string propertyName,
bool suppress)
{
var subject = Subject(
configureGenerator: c => c.NonNullableReferenceTypesAsRequired = true,
configureMvcOptions: o => o.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = suppress
);
var schemaRepository = new SchemaRepository();

subject.GenerateSchema(declaringType, schemaRepository);

var propertyIsRequired = schemaRepository.Schemas[subType].Required.Contains(propertyName);
Assert.Equal(!suppress, propertyIsRequired);
}

[Fact]
public void GenerateSchema_Works_IfNotProvidingMvcOptions()
{
var generatorOptions = new SchemaGeneratorOptions
{
NonNullableReferenceTypesAsRequired = true
};

var serializerOptions = new JsonSerializerOptions();

var subject = new SchemaGenerator(generatorOptions, new JsonSerializerDataContractResolver(serializerOptions));
var schemaRepository = new SchemaRepository();

subject.GenerateSchema(typeof(TypeWithNullableContext), schemaRepository);

var subType = nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent);
var propertyName = nameof(TypeWithNullableContext.NonNullableString);
var propertyIsRequired = schemaRepository.Schemas[subType].Required.Contains(propertyName);
Assert.True(propertyIsRequired);
}

[Fact]
public void GenerateSchema_HandlesTypesWithNestedTypes()
{
Expand Down Expand Up @@ -1004,15 +1067,19 @@ public void GenerateSchema_GeneratesSchema_IfParameterHasTypeConstraints()

private static SchemaGenerator Subject(
Action<SchemaGeneratorOptions> configureGenerator = null,
Action<JsonSerializerOptions> configureSerializer = null)
Action<JsonSerializerOptions> configureSerializer = null,
Action<MvcOptions> configureMvcOptions = null)
{
var generatorOptions = new SchemaGeneratorOptions();
configureGenerator?.Invoke(generatorOptions);

var serializerOptions = new JsonSerializerOptions();
configureSerializer?.Invoke(serializerOptions);

return new SchemaGenerator(generatorOptions, new JsonSerializerDataContractResolver(serializerOptions));
var mvcOptions = new MvcOptions();
configureMvcOptions?.Invoke(mvcOptions);

return new SchemaGenerator(generatorOptions, new JsonSerializerDataContractResolver(serializerOptions), Options.Create<MvcOptions>(mvcOptions));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;
using Swashbuckle.AspNetCore.TestSupport;
Expand Down Expand Up @@ -2235,7 +2236,7 @@ private static SwaggerGenerator Subject(
return new SwaggerGenerator(
options ?? DefaultOptions,
new FakeApiDescriptionGroupCollectionProvider(apiDescriptions),
new SchemaGenerator(new SchemaGeneratorOptions() { SchemaFilters = schemaFilters ?? [] }, new JsonSerializerDataContractResolver(new JsonSerializerOptions())),
new SchemaGenerator(new SchemaGeneratorOptions() { SchemaFilters = schemaFilters ?? [] }, new JsonSerializerDataContractResolver(new JsonSerializerOptions()), Options.Create<MvcOptions>(new MvcOptions())),
new FakeAuthenticationSchemeProvider(authenticationSchemes ?? Enumerable.Empty<AuthenticationScheme>())
);
}
Expand Down

0 comments on commit 85fefe4

Please sign in to comment.