Surface primary constructor parameter attributes in OpenAPI schemas#65804
Surface primary constructor parameter attributes in OpenAPI schemas#65804matantsach wants to merge 3 commits intodotnet:mainfrom
Conversation
For C# 12 class primary constructors, validation attributes like [Range], [Required], [MinLength] etc. exist only on the constructor parameter, not on the synthesized property. The OpenAPI schema generator reads attributes from JsonPropertyInfo.AttributeProvider (the PropertyInfo) and never sees them. Check JsonPropertyInfo.AssociatedParameter.AttributeProvider as a fallback for constructor parameter attributes. Property-level attributes still take precedence (applied second, overwriting). Also extends the TypeInfoResolver modifier to check constructor parameter attributes for [Required]. Fixes dotnet#61538
|
Thanks for your PR, @@matantsach. Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
There was a problem hiding this comment.
Pull request overview
This PR updates the OpenAPI schema generation pipeline to surface validation and descriptive metadata applied to C# 12 class primary-constructor parameters (which otherwise don’t get copied onto synthesized properties), ensuring generated schemas reflect those constraints.
Changes:
- Extend required-property detection to also consider
JsonPropertyInfo.AssociatedParameter.AttributeProviderfor[Required]. - Extend schema transformation to also read constructor-parameter
ValidationAttribute,DefaultValueAttribute, andDescriptionAttributemetadata (with property-level attributes taking precedence). - Add test coverage for validation/required/description attributes on primary constructor parameters, plus an override-precedence and record-regression test.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs |
Adds constructor-parameter attribute fallback for required detection and schema keyword mapping. |
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.PropertySchemas.cs |
Adds tests validating schema output for primary-constructor parameter attributes and precedence behavior. |
You can also share your feedback on Copilot code review. Take the survey.
| if (parameterAttributes.OfType<ValidationAttribute>() is { } paramValidationAttributes) | ||
| { | ||
| schema.ApplyValidationAttributes(paramValidationAttributes); | ||
| } |
There was a problem hiding this comment.
Good catch — though this pattern is inherited from the existing property-level block just below (line 151), which uses the same OfType<ValidationAttribute>() is { } guard. I kept it consistent with the surrounding code, but happy to clean up both if the team prefers.
| var isInlinedParamSchema = !schema.WillBeComponentized(); | ||
| if (isInlinedParamSchema) | ||
| { | ||
| if (parameterAttributes.OfType<DescriptionAttribute>().LastOrDefault() is { } paramDescriptionAttribute) | ||
| { | ||
| schema[OpenApiSchemaKeywords.DescriptionKeyword] = paramDescriptionAttribute.Description; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| if (parameterAttributes.OfType<DescriptionAttribute>().LastOrDefault() is { } paramDescriptionAttribute) | ||
| { | ||
| schema[OpenApiConstants.RefDescriptionAnnotation] = paramDescriptionAttribute.Description; | ||
| } |
There was a problem hiding this comment.
Same story here — the existing property-level block (lines 153-165) has the same duplicated DescriptionAttribute lookup across the inline/componentized branches. Kept it parallel for consistency, but I can consolidate both blocks if the team would like that as part of this PR.
|
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
Summary
C# 12 primary constructor parameters on classes carry their validation attributes (
[Range],[Required],[MinLength], etc.) only on theParameterInfo— unlike records, the compiler doesn't copy them to the synthesized property. The OpenAPI schema generator reads attributes exclusively fromJsonPropertyInfo.AttributeProvider(thePropertyInfo), so these constraints are silently dropped from the generated schema.This adds a fallback that checks
JsonPropertyInfo.AssociatedParameter.AttributeProviderfor constructor parameter attributes in two places:TypeInfoResolvermodifier — picks up[Required]from constructor parametersTransformSchemaNodecallback — picks upValidationAttribute,DefaultValueAttribute, andDescriptionAttributefrom constructor parametersConstructor parameter attributes are applied first; property-level attributes are applied second and take precedence. This means existing types with attributes on both (like records) continue to work correctly — the property-level copy wins, and idempotent re-application of the same values is harmless.
AOT considerations
AssociatedParameter.AttributeProviderfollows the same reflection pattern already used by the existingpropertyInfo.AttributeProviderpath. Under source-generated serialization contexts,AssociatedParameter.AttributeProviderisnull(same asAttributeProvider), so the new block is safely skipped. No new trimming or AOT risk surface is introduced.Test plan
HandlesValidationAttributesOnPrimaryConstructorParameters—[Range(0, 120)]and[MinLength(1), MaxLength(100)]on class primary constructor params produce correct schema constraintsHandlesRequiredAttributeOnPrimaryConstructorParameters—[Required]on constructor param marks property as required, with negative assertion on non-required propertyPropertyAttributeOverridesConstructorParameterAttribute— property-level[Range(10, 50)]correctly overrides constructor-level[Range(0, 100)]HandlesDescriptionAttributeOnPrimaryConstructorParameters—[Description]on constructor param appears in schemaRecordWithValidationAttributesStillWorks— record types are unaffected by the fallback (no double-application regression)Fixes #61538