diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs index 1a7bf31e09..c99f14adfe 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -185,6 +185,10 @@ private static void ApplyMinLengthAttribute(OpenApiSchema schema, MinLengthAttri { schema.MinItems = minLengthAttribute.Length; } + else if (schema.Type is { } objectType && objectType.HasFlag(JsonSchemaTypes.Object)) + { + schema.MinProperties = minLengthAttribute.Length; + } else { schema.MinLength = minLengthAttribute.Length; @@ -209,6 +213,10 @@ private static void ApplyMaxLengthAttribute(OpenApiSchema schema, MaxLengthAttri { schema.MaxItems = maxLengthAttribute.Length; } + else if (schema.Type is { } objectType && objectType.HasFlag(JsonSchemaTypes.Object)) + { + schema.MaxProperties = maxLengthAttribute.Length; + } else { schema.MaxLength = maxLengthAttribute.Length; @@ -234,6 +242,11 @@ private static void ApplyLengthAttribute(OpenApiSchema schema, LengthAttribute l schema.MinItems = lengthAttribute.MinimumLength; schema.MaxItems = lengthAttribute.MaximumLength; } + else if (schema.Type is { } objectType && objectType.HasFlag(JsonSchemaTypes.Object)) + { + schema.MinProperties = lengthAttribute.MinimumLength; + schema.MaxProperties = lengthAttribute.MaximumLength; + } else { schema.MinLength = lengthAttribute.MinimumLength; diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 6c0fbb3552..2af269e777 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -373,10 +373,16 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.Equal(3, schema.Properties["StringWithMinMaxLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithMinMaxLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithMinMaxLength"].MaxItems); + Assert.Equal(1, schema.Properties["BoundedReadOnlyDictionary"].MinProperties); + Assert.Equal(3, schema.Properties["BoundedReadOnlyDictionary"].MaxProperties); + Assert.Null(schema.Properties["BoundedReadOnlyDictionary"].MinLength); + Assert.Null(schema.Properties["BoundedReadOnlyDictionary"].MaxLength); Assert.Equal(1, schema.Properties["StringWithLength"].MinLength); Assert.Equal(3, schema.Properties["StringWithLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithLength"].MaxItems); + Assert.Equal(1, schema.Properties["BoundedDictionary"].MinProperties); + Assert.Equal(3, schema.Properties["BoundedDictionary"].MaxProperties); Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMinimum); Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMaximum); Assert.Equal("byte", schema.Properties["StringWithBase64"].Format); diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index d866a7b40c..9dc8f11c1f 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -373,10 +373,16 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.Equal(3, schema.Properties["StringWithMinMaxLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithMinMaxLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithMinMaxLength"].MaxItems); + Assert.Equal(1, schema.Properties["BoundedReadOnlyDictionary"].MinProperties); + Assert.Equal(3, schema.Properties["BoundedReadOnlyDictionary"].MaxProperties); + Assert.Null(schema.Properties["BoundedReadOnlyDictionary"].MinLength); + Assert.Null(schema.Properties["BoundedReadOnlyDictionary"].MaxLength); Assert.Equal(1, schema.Properties["StringWithLength"].MinLength); Assert.Equal(3, schema.Properties["StringWithLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithLength"].MaxItems); + Assert.Equal(1, schema.Properties["BoundedDictionary"].MinProperties); + Assert.Equal(3, schema.Properties["BoundedDictionary"].MaxProperties); Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMinimum); Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMaximum); Assert.Equal("byte", schema.Properties["StringWithBase64"].Format); diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/OpenApiSchemaExtensionsTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/OpenApiSchemaExtensionsTests.cs index 75e4f5fd8c..251d13b247 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/OpenApiSchemaExtensionsTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/OpenApiSchemaExtensionsTests.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using Microsoft.OpenApi; @@ -149,6 +150,189 @@ public static void ApplyValidationAttributes_Handles_DataTypeAttribute_CustomDat Assert.Equal(customDataType, schema.Format); } + [Fact] + public static void ApplyValidationAttributes_MinLength_On_Dictionary_Maps_To_MinProperties() + { + // Arrange - dictionary schema is represented as an Object with AdditionalProperties + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + AdditionalPropertiesAllowed = true, + AdditionalProperties = new OpenApiSchema { Type = JsonSchemaType.String }, + }; + + // Act + schema.ApplyValidationAttributes([new MinLengthAttribute(1)]); + + // Assert + Assert.Equal(1, schema.MinProperties); + Assert.Null(schema.MinLength); + Assert.Null(schema.MinItems); + } + + [Fact] + public static void ApplyValidationAttributes_MaxLength_On_Dictionary_Maps_To_MaxProperties() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + AdditionalPropertiesAllowed = true, + AdditionalProperties = new OpenApiSchema { Type = JsonSchemaType.String }, + }; + + // Act + schema.ApplyValidationAttributes([new MaxLengthAttribute(10)]); + + // Assert + Assert.Equal(10, schema.MaxProperties); + Assert.Null(schema.MaxLength); + Assert.Null(schema.MaxItems); + } + + [Fact] + public static void ApplyValidationAttributes_Length_On_Dictionary_Maps_To_Min_And_MaxProperties() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + AdditionalPropertiesAllowed = true, + AdditionalProperties = new OpenApiSchema { Type = JsonSchemaType.String }, + }; + + // Act + schema.ApplyValidationAttributes([new LengthAttribute(2, 5)]); + + // Assert + Assert.Equal(2, schema.MinProperties); + Assert.Equal(5, schema.MaxProperties); + Assert.Null(schema.MinLength); + Assert.Null(schema.MaxLength); + Assert.Null(schema.MinItems); + Assert.Null(schema.MaxItems); + } + + [Fact] + public static void ApplyValidationAttributes_MinLength_On_String_Still_Maps_To_MinLength() + { + // Arrange - regression guard for the existing string path + var schema = new OpenApiSchema { Type = JsonSchemaType.String }; + + // Act + schema.ApplyValidationAttributes([new MinLengthAttribute(3)]); + + // Assert + Assert.Equal(3, schema.MinLength); + Assert.Null(schema.MinProperties); + Assert.Null(schema.MinItems); + } + + [Fact] + public static void ApplyValidationAttributes_MinLength_On_Array_Still_Maps_To_MinItems() + { + // Arrange - regression guard for the existing array path + var schema = new OpenApiSchema { Type = JsonSchemaType.Array }; + + // Act + schema.ApplyValidationAttributes([new MinLengthAttribute(3)]); + + // Assert + Assert.Equal(3, schema.MinItems); + Assert.Null(schema.MinProperties); + Assert.Null(schema.MinLength); + } + + [Fact] + public static void ApplyValidationAttributes_MaxLength_On_String_Still_Maps_To_MaxLength() + { + var schema = new OpenApiSchema { Type = JsonSchemaType.String }; + + schema.ApplyValidationAttributes([new MaxLengthAttribute(5)]); + + Assert.Equal(5, schema.MaxLength); + Assert.Null(schema.MaxProperties); + Assert.Null(schema.MaxItems); + } + + [Fact] + public static void ApplyValidationAttributes_MaxLength_On_Array_Still_Maps_To_MaxItems() + { + var schema = new OpenApiSchema { Type = JsonSchemaType.Array }; + + schema.ApplyValidationAttributes([new MaxLengthAttribute(5)]); + + Assert.Equal(5, schema.MaxItems); + Assert.Null(schema.MaxProperties); + Assert.Null(schema.MaxLength); + } + + [Fact] + public static void ApplyValidationAttributes_Length_On_String_Still_Maps_To_MinAndMaxLength() + { + var schema = new OpenApiSchema { Type = JsonSchemaType.String }; + + schema.ApplyValidationAttributes([new LengthAttribute(1, 5)]); + + Assert.Equal(1, schema.MinLength); + Assert.Equal(5, schema.MaxLength); + Assert.Null(schema.MinProperties); + Assert.Null(schema.MaxProperties); + } + + [Fact] + public static void ApplyValidationAttributes_Length_On_Array_Still_Maps_To_MinAndMaxItems() + { + var schema = new OpenApiSchema { Type = JsonSchemaType.Array }; + + schema.ApplyValidationAttributes([new LengthAttribute(1, 5)]); + + Assert.Equal(1, schema.MinItems); + Assert.Equal(5, schema.MaxItems); + Assert.Null(schema.MinProperties); + Assert.Null(schema.MaxProperties); + } + + [Fact] + public static void ApplyValidationAttributes_MinLength_On_EnumKeyedDictionarySchema_Maps_To_MinProperties() + { + // Enum-keyed dictionaries are emitted as Object with known Properties and + // AdditionalPropertiesAllowed = false (see SchemaGenerator.CreateDictionarySchema). + // The fix must still route MinLength to MinProperties in this shape. + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["Foo"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["Bar"] = new OpenApiSchema { Type = JsonSchemaType.String }, + }, + AdditionalPropertiesAllowed = false, + }; + + schema.ApplyValidationAttributes([new MinLengthAttribute(1)]); + + Assert.Equal(1, schema.MinProperties); + Assert.Null(schema.MinLength); + } + + [Fact] + public static void ApplyValidationAttributes_MinLength_On_NullableObjectSchema_Maps_To_MinProperties() + { + // A nullable dictionary has Type = Object | Null. HasFlag(Object) must still route to MinProperties. + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object | JsonSchemaType.Null, + AdditionalPropertiesAllowed = true, + AdditionalProperties = new OpenApiSchema { Type = JsonSchemaType.String }, + }; + + schema.ApplyValidationAttributes([new MinLengthAttribute(2)]); + + Assert.Equal(2, schema.MinProperties); + Assert.Null(schema.MinLength); + } + private sealed class CultureSwitcher : IDisposable { private readonly CultureInfo _previous; diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs index 913f5d677f..0de5f00eb4 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs @@ -1135,6 +1135,40 @@ public async Task ActionHavingFromFormAttributeWithSwaggerIgnore() await Verify(document); } + [Theory] + [InlineData(typeof(TypeWithValidationAttributes))] + [InlineData(typeof(TypeWithValidationAttributesViaMetadataType))] + public async Task GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties(Type type) + { + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithNoParameters), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: + [ + new ApiParameterDescription + { + Name = "body", + Source = BindingSource.Body, + ModelMetadata = ModelMetadataFactory.CreateForType(type), + Type = type + } + ], + supportedRequestFormats: [new ApiRequestFormat { MediaType = "application/json" }]) + ] + ); + + var document = subject.GetSwagger("v1"); + + await Verifier.Verify(ToJson(document)) + .UseDirectory(SnapshotsDirectory) + .UseTextForParameters(type.Name); + } + [Fact] public async Task GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism() { diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt new file mode 100644 index 0000000000..7f618a196f --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributes" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributes": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "integer", + "format": "int32" + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt new file mode 100644 index 0000000000..427d665536 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/10_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributesViaMetadataType" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributesViaMetadataType": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "number", + "format": "double" + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt new file mode 100644 index 0000000000..7f618a196f --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributes" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributes": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "integer", + "format": "int32" + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt new file mode 100644 index 0000000000..427d665536 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/8_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributesViaMetadataType" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributesViaMetadataType": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "number", + "format": "double" + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt new file mode 100644 index 0000000000..7f618a196f --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributes.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributes" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributes": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "integer", + "format": "int32" + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt new file mode 100644 index 0000000000..427d665536 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/snapshots/9_0/VerifyTests.GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties_TypeWithValidationAttributesViaMetadataType.verified.txt @@ -0,0 +1,164 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Test API", + "version": "V1" + }, + "paths": { + "/resource": { + "post": { + "tags": [ + "Fake" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithValidationAttributesViaMetadataType" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "IntEnum": { + "enum": [ + 2, + 4, + 8 + ], + "type": "integer", + "format": "int32" + }, + "TypeWithValidationAttributesViaMetadataType": { + "required": [ + "NullableIntEnumWithRequired", + "StringWithRequired", + "StringWithRequiredAllowEmptyTrue" + ], + "type": "object", + "properties": { + "StringWithDataTypeCreditCard": { + "type": "string", + "format": "credit-card", + "nullable": true + }, + "StringWithMinMaxLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithMinMaxLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedReadOnlyDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithLength": { + "maxLength": 3, + "minLength": 1, + "type": "string", + "nullable": true + }, + "ArrayWithLength": { + "maxItems": 3, + "minItems": 1, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BoundedDictionary": { + "maxProperties": 3, + "minProperties": 1, + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "StringWithBase64": { + "type": "string", + "format": "byte", + "nullable": true + }, + "IntWithExclusiveRange": { + "maximum": 10, + "exclusiveMaximum": true, + "minimum": 1, + "exclusiveMinimum": true, + "type": "number", + "format": "double" + }, + "IntWithRange": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "StringWithRegularExpression": { + "pattern": "^[3-6]?\\d{12,15}$", + "type": "string", + "nullable": true + }, + "StringWithStringLength": { + "maxLength": 10, + "minLength": 5, + "type": "string", + "nullable": true + }, + "StringWithRequired": { + "minLength": 1, + "type": "string" + }, + "StringWithRequiredAllowEmptyTrue": { + "type": "string" + }, + "StringWithDescription": { + "type": "string", + "description": "Description", + "nullable": true + }, + "StringWithReadOnly": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NullableIntEnumWithRequired": { + "$ref": "#/components/schemas/IntEnum" + } + }, + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "Fake" + } + ] +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs index 7e066b9480..0610d1ef4c 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Swashbuckle.AspNetCore.TestSupport; @@ -14,12 +15,18 @@ public class TypeWithValidationAttributes [MinLength(1), MaxLength(3)] public string[] ArrayWithMinMaxLength { get; set; } + [MinLength(1), MaxLength(3)] + public IReadOnlyDictionary BoundedReadOnlyDictionary { get; set; } + [Length(1, 3)] public string StringWithLength { get; set; } [Length(1, 3)] public string[] ArrayWithLength { get; set; } + [Length(1, 3)] + public Dictionary BoundedDictionary { get; set; } + [Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)] public int IntWithExclusiveRange { get; set; } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs index 735f944f4a..68632e06fd 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; @@ -13,10 +14,14 @@ public class TypeWithValidationAttributesViaMetadataType public string[] ArrayWithMinMaxLength { get; set; } + public IReadOnlyDictionary BoundedReadOnlyDictionary { get; set; } + public string StringWithLength { get; set; } public string[] ArrayWithLength { get; set; } + public Dictionary BoundedDictionary { get; set; } + public string StringWithBase64 { get; set; } public double IntWithExclusiveRange { get; set; } @@ -49,12 +54,18 @@ public class MetadataType [MinLength(1), MaxLength(3)] public string[] ArrayWithMinMaxLength { get; set; } + [MinLength(1), MaxLength(3)] + public IReadOnlyDictionary BoundedReadOnlyDictionary { get; set; } + [Length(1, 3)] public string StringWithLength { get; set; } [Length(1, 3)] public string[] ArrayWithLength { get; set; } + [Length(1, 3)] + public Dictionary BoundedDictionary { get; set; } + [Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)] public int IntWithExclusiveRange { get; set; }