Skip to content

Commit c7fc087

Browse files
authored
OpenAPI: Apply descriptions from [Description] to the schema reference instead of the actual schema for properties (#63177)
* Add Handling of the special x-reference- attributes in the schema exporter * Add unit tests for schema annotations comming from [Description] attributes * Add applying annotations with [Description] attributes for types * Remove magic strings and simplify null checks for attributes * Retrieve the property attributes once when transforming schema nodes * Remove deserialization of special x-ref-example as it's not used in that code path. * Limit the isInlinedSchema check to the OpenApi annotation "description"
1 parent 8f2ac0a commit c7fc087

File tree

3 files changed

+169
-4
lines changed

3 files changed

+169
-4
lines changed

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
345345
schema.Metadata ??= new Dictionary<string, object>();
346346
schema.Metadata[OpenApiConstants.RefId] = reader.GetString() ?? string.Empty;
347347
break;
348+
case OpenApiConstants.RefDescriptionAnnotation:
349+
reader.Read();
350+
schema.Metadata ??= new Dictionary<string, object>();
351+
schema.Metadata[OpenApiConstants.RefDescriptionAnnotation] = reader.GetString() ?? string.Empty;
352+
break;
353+
348354
default:
349355
reader.Skip();
350356
break;

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,35 @@ internal sealed class OpenApiSchemaService(
101101
{
102102
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
103103
}
104+
if (context.TypeInfo.Type.GetCustomAttributes(inherit: false).OfType<DescriptionAttribute>().LastOrDefault() is { } typeDescriptionAttribute)
105+
{
106+
schema[OpenApiSchemaKeywords.DescriptionKeyword] = typeDescriptionAttribute.Description;
107+
}
104108
if (context.PropertyInfo is { AttributeProvider: { } attributeProvider })
105109
{
106-
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ValidationAttribute>() is { } validationAttributes)
110+
var propertyAttributes = attributeProvider.GetCustomAttributes(inherit: false);
111+
if (propertyAttributes.OfType<ValidationAttribute>() is { } validationAttributes)
107112
{
108113
schema.ApplyValidationAttributes(validationAttributes);
109114
}
110-
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<DefaultValueAttribute>().LastOrDefault() is DefaultValueAttribute defaultValueAttribute)
115+
if (propertyAttributes.OfType<DefaultValueAttribute>().LastOrDefault() is { } defaultValueAttribute)
111116
{
112117
schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo);
113118
}
114-
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<DescriptionAttribute>().LastOrDefault() is DescriptionAttribute descriptionAttribute)
119+
var isInlinedSchema = schema[OpenApiConstants.SchemaId] is null;
120+
if (isInlinedSchema)
121+
{
122+
if (propertyAttributes.OfType<DescriptionAttribute>().LastOrDefault() is { } descriptionAttribute)
123+
{
124+
schema[OpenApiSchemaKeywords.DescriptionKeyword] = descriptionAttribute.Description;
125+
}
126+
}
127+
else
115128
{
116-
schema[OpenApiSchemaKeywords.DescriptionKeyword] = descriptionAttribute.Description;
129+
if (propertyAttributes.OfType<DescriptionAttribute>().LastOrDefault() is { } descriptionAttribute)
130+
{
131+
schema[OpenApiConstants.RefDescriptionAnnotation] = descriptionAttribute.Description;
132+
}
117133
}
118134
}
119135

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using System.Net.Http;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.OpenApi;
8+
using Microsoft.AspNetCore.Routing;
9+
10+
public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase
11+
{
12+
[Fact]
13+
public async Task SchemaDescriptions_HandlesSchemaReferences()
14+
{
15+
// Arrange
16+
var builder = CreateBuilder();
17+
18+
// Act
19+
builder.MapPost("/", (DescribedReferencesDto dto) => { });
20+
21+
// Assert
22+
await VerifyOpenApiDocument(builder, document =>
23+
{
24+
var operation = document.Paths["/"].Operations[HttpMethod.Post];
25+
var requestBody = operation.RequestBody;
26+
27+
Assert.NotNull(requestBody);
28+
var content = Assert.Single(requestBody.Content);
29+
Assert.Equal("application/json", content.Key);
30+
Assert.NotNull(content.Value.Schema);
31+
var schema = content.Value.Schema;
32+
Assert.Equal(JsonSchemaType.Object, schema.Type);
33+
Assert.Collection(schema.Properties,
34+
property =>
35+
{
36+
Assert.Equal("child1", property.Key);
37+
var reference = Assert.IsType<OpenApiSchemaReference>(property.Value);
38+
Assert.Equal("Property: DescribedReferencesDto.Child1", reference.Reference.Description);
39+
},
40+
property =>
41+
{
42+
Assert.Equal("child2", property.Key);
43+
var reference = Assert.IsType<OpenApiSchemaReference>(property.Value);
44+
Assert.Equal("Property: DescribedReferencesDto.Child2", reference.Reference.Description);
45+
},
46+
property =>
47+
{
48+
Assert.Equal("childNoDescription", property.Key);
49+
var reference = Assert.IsType<OpenApiSchemaReference>(property.Value);
50+
Assert.Null(reference.Reference.Description);
51+
});
52+
53+
var referencedSchema = document.Components.Schemas["DescribedChildDto"];
54+
Assert.Equal("Class: DescribedChildDto", referencedSchema.Description);
55+
});
56+
57+
}
58+
59+
[Description("Class: DescribedReferencesDto")]
60+
public class DescribedReferencesDto
61+
{
62+
[Description("Property: DescribedReferencesDto.Child1")]
63+
public DescribedChildDto Child1 { get; set; }
64+
65+
[Description("Property: DescribedReferencesDto.Child2")]
66+
public DescribedChildDto Child2 { get; set; }
67+
68+
public DescribedChildDto ChildNoDescription { get; set; }
69+
}
70+
71+
[Description("Class: DescribedChildDto")]
72+
public class DescribedChildDto
73+
{
74+
[Description("Property: DescribedChildDto.ChildValue")]
75+
public string ChildValue { get; set; }
76+
}
77+
78+
[Fact]
79+
public async Task SchemaDescriptions_HandlesInlinedSchemas()
80+
{
81+
// Arrange
82+
var builder = CreateBuilder();
83+
84+
var options = new OpenApiOptions();
85+
var originalCreateSchemaReferenceId = options.CreateSchemaReferenceId;
86+
options.CreateSchemaReferenceId = (x) => x.Type == typeof(DescribedInlinedDto) ? null : originalCreateSchemaReferenceId(x);
87+
88+
// Act
89+
builder.MapPost("/", (DescribedInlinedSchemasDto dto) => { });
90+
91+
// Assert
92+
await VerifyOpenApiDocument(builder, options, document =>
93+
{
94+
var operation = document.Paths["/"].Operations[HttpMethod.Post];
95+
var requestBody = operation.RequestBody;
96+
97+
Assert.NotNull(requestBody);
98+
var content = Assert.Single(requestBody.Content);
99+
Assert.Equal("application/json", content.Key);
100+
Assert.NotNull(content.Value.Schema);
101+
var schema = content.Value.Schema;
102+
Assert.Equal(JsonSchemaType.Object, schema.Type);
103+
Assert.Collection(schema.Properties,
104+
property =>
105+
{
106+
Assert.Equal("inlined1", property.Key);
107+
var inlinedSchema = Assert.IsType<OpenApiSchema>(property.Value);
108+
Assert.Equal("Property: DescribedInlinedSchemasDto.Inlined1", inlinedSchema.Description);
109+
},
110+
property =>
111+
{
112+
Assert.Equal("inlined2", property.Key);
113+
var inlinedSchema = Assert.IsType<OpenApiSchema>(property.Value);
114+
Assert.Equal("Property: DescribedInlinedSchemasDto.Inlined2", inlinedSchema.Description);
115+
},
116+
property =>
117+
{
118+
Assert.Equal("inlinedNoDescription", property.Key);
119+
var inlinedSchema = Assert.IsType<OpenApiSchema>(property.Value);
120+
Assert.Equal("Class: DescribedInlinedDto", inlinedSchema.Description);
121+
});
122+
});
123+
}
124+
125+
[Description("Class: DescribedInlinedSchemasDto")]
126+
public class DescribedInlinedSchemasDto
127+
{
128+
[Description("Property: DescribedInlinedSchemasDto.Inlined1")]
129+
public DescribedInlinedDto Inlined1 { get; set; }
130+
131+
[Description("Property: DescribedInlinedSchemasDto.Inlined2")]
132+
public DescribedInlinedDto Inlined2 { get; set; }
133+
134+
public DescribedInlinedDto InlinedNoDescription { get; set; }
135+
}
136+
137+
[Description("Class: DescribedInlinedDto")]
138+
public class DescribedInlinedDto
139+
{
140+
[Description("Property: DescribedInlinedDto.ChildValue")]
141+
public string ChildValue { get; set; }
142+
}
143+
}

0 commit comments

Comments
 (0)