Map [MinLength]/[MaxLength] on dictionary properties to minProperties / maxProperties#3922
Conversation
…erties / maxProperties Fixes domaindrivendev#3915. OpenAPI defines minLength/maxLength only for string schemas; for object schemas (including dictionaries) the equivalent constraints are minProperties/maxProperties. Before this change, applying [MinLength(1)] to a property typed as IReadOnlyDictionary<TKey, TValue> produced "minLength": 1 on the schema, which is invalid per the spec and is flagged by linters such as vacuum. The three attribute handlers (MinLength, MaxLength, Length) already switch to MinItems/MaxItems when the schema is an Array. Extend the same branching to recognise Object schemas and emit MinProperties/MaxProperties instead of MinLength/MaxLength. Route constraint handlers (MinLengthRouteConstraint, MaxLengthRouteConstraint) are intentionally left alone: route parameters are primitives and cannot be dictionaries, so the existing two-branch array/string logic is correct there. Tests added in OpenApiSchemaExtensionsTests: - MinLength on dictionary → minProperties - MaxLength on dictionary → maxProperties - Length on dictionary → min+maxProperties - Regression guards: MinLength on string still maps to minLength, on array still maps to minItems.
Added: - End-to-end coverage via shared TypeWithValidationAttributes fixture — DictionaryWithMinMaxLength ([MinLength(1),MaxLength(3)]) and DictionaryWithLength ([Length(1,3)]) properties, asserted by both JsonSerializerSchemaGenerator and NewtonsoftSchemaGenerator tests. Reproduces the exact scenario from the issue (dictionary property on a model class) rather than only exercising ApplyValidationAttributes in isolation. - Enum-keyed dictionary shape — Object schema with known Properties and AdditionalPropertiesAllowed=false (see CreateDictionarySchema). MinLength must still route to MinProperties here. - Nullable dictionary shape — Type = Object | Null. HasFlag(Object) must still route to MinProperties. - Symmetry regression guards: MaxLength/Length on string map to MinLength/MaxLength, on array map to MinItems/MaxItems (previously only MinLength had explicit guards). Fixture updates are picked up by both Newtonsoft and JsonSerializer generator test suites because the shared TestSupport fixture drives both.
martincostello
left a comment
There was a problem hiding this comment.
Please make some test changes to that the OpenAPI snapshots demonstrate the required changes to the generated OpenAPI schemas.
- Replace em-dashes with hyphens in OpenApiSchemaExtensionsTests.cs - Rename fixture property DictionaryWithMinMaxLength to IReadOnlyDictionaryWithMinMaxLength so the name matches its actual IReadOnlyDictionary<,> declared type - Add JSON-output test that serializes the schema for TypeWithValidationAttributes/TypeWithValidationAttributesViaMetadataType to OpenAPI 3.0 JSON and asserts the dictionary properties surface as minProperties/maxProperties in the produced OpenAPI document
…ttribute Per review: 'Length' as a noun does not describe a dictionary (which has a property/item count, not a length). The Attribute suffix makes it explicit that the property exists to test the [Length] attribute mapping rather than a conceptual 'length' of the dictionary.
Per follow-up review: in TypeWithValidationAttributesViaMetadataType the outer class is a bare property list (attributes live on the inner MetadataType class), so names that referenced attributes were misleading. Renamed to neutral, type-focused names that work in both fixture variants: - IReadOnlyDictionaryWithMinMaxLength -> BoundedReadOnlyDictionary - DictionaryWithLengthAttribute -> BoundedDictionary The bounded prefix describes the test purpose (a dictionary whose size is constrained); the readonly/mutable distinction reflects the actual type under test. No attribute presence is claimed by the property name.
…ictionaries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3922 +/- ##
=======================================
Coverage 95.04% 95.05%
=======================================
Files 111 111
Lines 3958 3965 +7
Branches 801 804 +3
=======================================
+ Hits 3762 3769 +7
Misses 196 196
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Tests are failing. |
The Verify rework only added baselines under snapshots/10_0/, causing GenerateSchema_BoundedDictionaries_UsesMinAndMaxProperties to fail on net8.0 and net9.0. Schemas are framework-agnostic, so the snapshots match the existing net10.0 baselines verbatim.
|
@martincostello pushed 2d8e31e - the snapshot rework (743061c) only added baselines under |
The Theory parameter Type type was serialized into snapshot filenames via .UseParameters(type), producing names like ..._type=Swashbuckle.AspNetCore.TestSupport.TypeWithValidationAttributesViaMetadataType.verified.txt. Combined with the Windows runner checkout root and snapshots/<fw>/ path, the total exceeded the 260-character MAX_PATH limit and git checkout failed with "Filename too long" on windows-latest while macos/ubuntu passed. Replace with .UseTextForParameters(type.Name) to drop the namespace prefix from the filename and rename the 6 existing snapshots accordingly. Content of the snapshots is unchanged.
|
Pushed 54c1cee. The earlier fix in 2d8e31e (copying snapshots to This commit replaces |
|
Thanks for your contribution @KitKeen - the changes from this pull request have been published as part of version 10.2.2 📦, which is now available from NuGet.org 🚀 |
Fixes #3915.
Problem
Applying
[MinLength(1)]to a property typed as a dictionary (IDictionary<,>,IReadOnlyDictionary<,>, etc.) produces"minLength": 1on the generated schema:"minLength"on an object schema is invalid per the OpenAPI 3.x spec — it's defined only for string schemas. Linters such asvacuumflag this (the original reporter hit it exactly that way). The equivalent constraint for object schemas is"minProperties".Fix
OpenApiSchemaExtensions.Apply{Min,Max,}LengthAttributealready branches onschema.Typeto pickMinItems/MaxItemsfor arrays. This PR adds a parallel branch forJsonSchemaTypes.Objectso dictionary schemas getMinProperties/MaxPropertiesinstead of the meaninglessMinLength/MaxLength.No changes to the
MinLengthRouteConstraint/MaxLengthRouteConstrainthandlers — route parameters are primitives and cannot be dictionaries, so the existing Array/otherwise split is correct there.Before / after
Before:
After:
Tests
Added to
OpenApiSchemaExtensionsTests:MinLengthon dictionary schema →minPropertiesMaxLengthon dictionary schema →maxPropertiesLength(min,max)on dictionary schema →min+maxPropertiesMinLengthon string schema still maps tominLength; on array schema still maps tominItems.Scope
TypeincludesObject— previously those would receiveMinLength/MaxLength, which was invalid per the OpenAPI spec, so any consumer relying on the old output was already producing non-conforming documents.Checklist