Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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["IReadOnlyDictionaryWithMinMaxLength"].MinProperties);
Assert.Equal(3, schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].MaxProperties);
Assert.Null(schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].MinLength);
Assert.Null(schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].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["DictionaryWithLengthAttribute"].MinProperties);
Assert.Equal(3, schema.Properties["DictionaryWithLengthAttribute"].MaxProperties);
Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMinimum);
Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMaximum);
Assert.Equal("byte", schema.Properties["StringWithBase64"].Format);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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["IReadOnlyDictionaryWithMinMaxLength"].MinProperties);
Assert.Equal(3, schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].MaxProperties);
Assert.Null(schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].MinLength);
Assert.Null(schema.Properties["IReadOnlyDictionaryWithMinMaxLength"].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["DictionaryWithLengthAttribute"].MinProperties);
Assert.Equal(3, schema.Properties["DictionaryWithLengthAttribute"].MaxProperties);
Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMinimum);
Assert.NotNull(schema.Properties["IntWithExclusiveRange"].ExclusiveMaximum);
Assert.Equal("byte", schema.Properties["StringWithBase64"].Format);
Expand All @@ -397,6 +403,33 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt
Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly);
}

[Theory]
[InlineData(typeof(TypeWithValidationAttributes))]
[InlineData(typeof(TypeWithValidationAttributesViaMetadataType))]
public void GenerateSchema_DictionaryWithLengthAttributes_OpenApiJsonUsesMinAndMaxProperties(Type type)
{
// Arrange
var schemaRepository = new SchemaRepository();
Subject().GenerateSchema(type, schemaRepository);

var document = new OpenApiDocument
{
Components = new OpenApiComponents { Schemas = schemaRepository.Schemas },
};

// Act - serialize to OpenAPI 3.0 JSON, the same writer used by the runtime
using var writer = new StringWriter();
var jsonWriter = new OpenApiJsonWriter(writer);
document.SerializeAsV3(jsonWriter);
var json = writer.ToString();

// Assert - the dictionary constraints surface as minProperties/maxProperties
// in the generated OpenAPI document. Before the fix these would have been
// emitted as minLength/maxLength on an object schema, which is invalid per spec.
Assert.Contains("\"minProperties\": 1", json);
Assert.Contains("\"maxProperties\": 3", json);
}

[Fact]
public void GenerateSchema_SetsReadOnlyAndWriteOnlyFlags_IfPropertyIsRestricted()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.OpenApi;

Expand Down Expand Up @@ -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<string, IOpenApiSchema>
{
["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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Swashbuckle.AspNetCore.TestSupport;
Expand All @@ -14,12 +15,18 @@ public class TypeWithValidationAttributes
[MinLength(1), MaxLength(3)]
public string[] ArrayWithMinMaxLength { get; set; }

[MinLength(1), MaxLength(3)]
public IReadOnlyDictionary<string, string> IReadOnlyDictionaryWithMinMaxLength { get; set; }

[Length(1, 3)]
public string StringWithLength { get; set; }

[Length(1, 3)]
public string[] ArrayWithLength { get; set; }

[Length(1, 3)]
public Dictionary<string, string> DictionaryWithLengthAttribute { get; set; }

[Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)]
public int IntWithExclusiveRange { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -13,10 +14,14 @@ public class TypeWithValidationAttributesViaMetadataType

public string[] ArrayWithMinMaxLength { get; set; }

public IReadOnlyDictionary<string, string> IReadOnlyDictionaryWithMinMaxLength { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there's no min or max.


public string StringWithLength { get; set; }

public string[] ArrayWithLength { get; set; }

public Dictionary<string, string> DictionaryWithLengthAttribute { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it doesn't have an attribute...


public string StringWithBase64 { get; set; }

public double IntWithExclusiveRange { get; set; }
Expand Down Expand Up @@ -49,12 +54,18 @@ public class MetadataType
[MinLength(1), MaxLength(3)]
public string[] ArrayWithMinMaxLength { get; set; }

[MinLength(1), MaxLength(3)]
public IReadOnlyDictionary<string, string> IReadOnlyDictionaryWithMinMaxLength { get; set; }

[Length(1, 3)]
public string StringWithLength { get; set; }

[Length(1, 3)]
public string[] ArrayWithLength { get; set; }

[Length(1, 3)]
public Dictionary<string, string> DictionaryWithLengthAttribute { get; set; }

[Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)]
public int IntWithExclusiveRange { get; set; }

Expand Down