Skip to content

Commit 8a23f0a

Browse files
authored
Add support for required keyword (#2810)
Add support for `required` keyword.
1 parent 1b7957b commit 8a23f0a

File tree

6 files changed

+111
-10
lines changed

6 files changed

+111
-10
lines changed

src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,25 @@ private OpenApiSchema GenerateSchemaForMember(
6767
if (dataProperty != null)
6868
{
6969
var requiredAttribute = customAttributes.OfType<RequiredAttribute>().FirstOrDefault();
70+
71+
schema.ReadOnly = dataProperty.IsReadOnly;
72+
schema.WriteOnly = dataProperty.IsWriteOnly;
73+
74+
#if NET7_0_OR_GREATER
75+
var hasRequiredMemberAttribute = customAttributes.OfType<System.Runtime.CompilerServices.RequiredMemberAttribute>().Any();
76+
77+
schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes
78+
? dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType()
79+
: dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute;
80+
81+
schema.MinLength = modelType == typeof(string) && (hasRequiredMemberAttribute || requiredAttribute is { AllowEmptyStrings: false }) ? 1 : null;
82+
#else
7083
schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes
7184
? dataProperty.IsNullable && requiredAttribute==null && !memberInfo.IsNonNullableReferenceType()
7285
: dataProperty.IsNullable && requiredAttribute==null;
7386

74-
schema.ReadOnly = dataProperty.IsReadOnly;
75-
schema.WriteOnly = dataProperty.IsWriteOnly;
7687
schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null;
88+
#endif
7789
}
7890

7991
var defaultValueAttribute = customAttributes.OfType<DefaultValueAttribute>().FirstOrDefault();
@@ -392,7 +404,13 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi
392404
? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty)
393405
: GenerateSchemaForType(dataProperty.MemberType, schemaRepository);
394406

395-
if ((dataProperty.IsRequired || customAttributes.OfType<RequiredAttribute>().Any())
407+
if ((
408+
dataProperty.IsRequired
409+
|| customAttributes.OfType<RequiredAttribute>().Any()
410+
#if NET7_0_OR_GREATER
411+
|| customAttributes.OfType<System.Runtime.CompilerServices.RequiredMemberAttribute>().Any()
412+
#endif
413+
)
396414
&& !schema.Required.Contains(dataProperty.Name))
397415
{
398416
schema.Required.Add(dataProperty.Name);

src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ public static class ApiParameterDescriptionExtensions
1515
private static readonly Type[] RequiredAttributeTypes = new[]
1616
{
1717
typeof(BindRequiredAttribute),
18-
typeof(RequiredAttribute)
18+
typeof(RequiredAttribute),
19+
#if NET7_0_OR_GREATER
20+
typeof(System.Runtime.CompilerServices.RequiredMemberAttribute)
21+
#endif
1922
};
2023

2124
public static bool IsRequiredParameter(this ApiParameterDescription apiParameter)
@@ -109,4 +112,4 @@ internal static bool IsFromForm(this ApiParameterDescription apiParameter)
109112
|| (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType));
110113
}
111114
}
112-
}
115+
}

test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ public void ActionWithIntParameterWithRequiredAttribute([Required]int param)
5252
public void ActionWithObjectParameter(XmlAnnotatedType param)
5353
{ }
5454

55+
#if NET7_0_OR_GREATER
56+
public class TypeWithRequiredProperty
57+
{
58+
public required string RequiredProperty { get; set; }
59+
}
60+
61+
public void ActionWithRequiredMember(TypeWithRequiredProperty param)
62+
{ }
63+
#endif
64+
5565
[Consumes("application/someMediaType")]
5666
public void ActionWithConsumesAttribute(string param)
5767
{ }
@@ -67,4 +77,4 @@ public int ActionWithProducesAttribute()
6777
throw new NotImplementedException();
6878
}
6979
}
70-
}
80+
}

test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.ComponentModel.DataAnnotations;
45
using System.Dynamic;
56
using System.Linq;
7+
using System.Net;
68
using System.Text.Json;
79
using System.Text.Json.Serialization;
8-
using System.Net;
910
using Microsoft.AspNetCore.Http;
1011
using Microsoft.AspNetCore.Mvc;
1112
using Microsoft.AspNetCore.Mvc.ApiExplorer;
1213
using Microsoft.AspNetCore.Routing;
1314
using Microsoft.AspNetCore.Routing.Constraints;
15+
using Microsoft.OpenApi.Any;
1416
using Microsoft.OpenApi.Models;
15-
using Xunit;
1617
using Swashbuckle.AspNetCore.TestSupport;
17-
using Microsoft.OpenApi.Any;
18+
using Xunit;
1819

1920
namespace Swashbuckle.AspNetCore.SwaggerGen.Test
2021
{
@@ -343,6 +344,43 @@ public void GenerateSchema_SetsReadOnlyAndWriteOnlyFlags_IfPropertyIsRestricted(
343344
Assert.True(schema.Properties["WriteOnlyProperty"].WriteOnly);
344345
}
345346

347+
#if NET7_0_OR_GREATER
348+
public class TypeWithRequiredProperty
349+
{
350+
public required string RequiredProperty { get; set; }
351+
}
352+
353+
public class TypeWithRequiredPropertyAndValidationAttribute
354+
{
355+
[MinLength(1)]
356+
public required string RequiredProperty { get; set; }
357+
}
358+
359+
[Fact]
360+
public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeyword()
361+
{
362+
var schemaRepository = new SchemaRepository();
363+
364+
var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredProperty), schemaRepository);
365+
366+
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
367+
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
368+
}
369+
370+
[Fact]
371+
public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidationAttribute()
372+
{
373+
var schemaRepository = new SchemaRepository();
374+
375+
var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredPropertyAndValidationAttribute), schemaRepository);
376+
377+
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
378+
Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength);
379+
Assert.False(schema.Properties["RequiredProperty"].Nullable);
380+
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
381+
}
382+
#endif
383+
346384
[Theory]
347385
[InlineData(typeof(TypeWithParameterizedConstructor), nameof(TypeWithParameterizedConstructor.Id), false)]
348386
[InlineData(typeof(TypeWithParameterlessAndParameterizedConstructor), nameof(TypeWithParameterlessAndParameterizedConstructor.Id), true)]

test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,38 @@ public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredOrBindR
514514
Assert.Equal(expectedRequired, parameter.Required);
515515
}
516516

517+
#if NET7_0_OR_GREATER
518+
[Fact]
519+
public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredMember()
520+
{
521+
var subject = Subject(
522+
apiDescriptions: new[]
523+
{
524+
ApiDescriptionFactory.Create(
525+
methodInfo: typeof(FakeController).GetMethod(nameof(FakeController.ActionWithRequiredMember)),
526+
groupName: "v1",
527+
httpMethod: "POST",
528+
relativePath: "resource",
529+
parameterDescriptions: new []
530+
{
531+
new ApiParameterDescription
532+
{
533+
Name = "param",
534+
Source = BindingSource.Query,
535+
ModelMetadata = ModelMetadataFactory.CreateForProperty(typeof(FakeController.TypeWithRequiredProperty), "RequiredProperty")
536+
}
537+
})
538+
}
539+
);
540+
541+
var document = subject.GetSwagger("v1");
542+
543+
var operation = document.Paths["/resource"].Operations[OperationType.Post];
544+
var parameter = Assert.Single(operation.Parameters);
545+
Assert.True(parameter.Required);
546+
}
547+
#endif
548+
517549
[Theory]
518550
[InlineData(false)]
519551
[InlineData(true)]

test/Swashbuckle.AspNetCore.TestSupport/ApiExplorer/ApiDescriptionFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static ApiDescription Create(
4646
ControllerParameterDescriptor;
4747
#endif
4848

49-
if (parameterDescriptorWithParameterInfo != null)
49+
if (parameterDescriptorWithParameterInfo != null && parameter.ModelMetadata == null)
5050
{
5151
parameter.ModelMetadata = ModelMetadataFactory.CreateForParameter(parameterDescriptorWithParameterInfo.ParameterInfo);
5252
}

0 commit comments

Comments
 (0)