Skip to content

Commit bb49884

Browse files
Support [Description] and [ReadOnly] (#3162)
Support `[Description]` and `[ReadOnly]` attributes.
1 parent e727e8d commit bb49884

8 files changed

Lines changed: 83 additions & 37 deletions

File tree

src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.ComponentModel;
23
using System.ComponentModel.DataAnnotations;
34
using Microsoft.AspNetCore.Mvc.ApiExplorer;
45
using Microsoft.AspNetCore.Routing.Constraints;
@@ -60,6 +61,13 @@ public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumera
6061

6162
else if (attribute is StringLengthAttribute stringLengthAttribute)
6263
ApplyStringLengthAttribute(schema, stringLengthAttribute);
64+
65+
else if (attribute is ReadOnlyAttribute readOnlyAttribute)
66+
ApplyReadOnlyAttribute(schema, readOnlyAttribute);
67+
68+
else if (attribute is DescriptionAttribute descriptionAttribute)
69+
ApplyDescriptionAttribute(schema, descriptionAttribute);
70+
6371
}
6472
}
6573

@@ -230,6 +238,16 @@ private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengt
230238
schema.MaxLength = stringLengthAttribute.MaximumLength;
231239
}
232240

241+
private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribute readOnlyAttribute)
242+
{
243+
schema.ReadOnly = readOnlyAttribute.IsReadOnly;
244+
}
245+
246+
private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute)
247+
{
248+
schema.Description ??= descriptionAttribute.Description;
249+
}
250+
233251
private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint)
234252
{
235253
schema.MinLength = lengthRouteConstraint.MinLength;

test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@
972972
"properties": {
973973
"currencyFrom": {
974974
"type": "string",
975+
"description": "Currency From",
975976
"nullable": true
976977
},
977978
"currencyTo": {
@@ -1017,6 +1018,10 @@
10171018
"$ref": "#/components/schemas/CurrenciesRate"
10181019
},
10191020
"nullable": true
1021+
},
1022+
"isUpdated": {
1023+
"type": "boolean",
1024+
"readOnly": true
10201025
}
10211026
},
10221027
"additionalProperties": false

test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@
972972
"properties": {
973973
"currencyFrom": {
974974
"type": "string",
975+
"description": "Currency From",
975976
"nullable": true
976977
},
977978
"currencyTo": {
@@ -1017,6 +1018,10 @@
10171018
"$ref": "#/components/schemas/CurrenciesRate"
10181019
},
10191020
"nullable": true
1021+
},
1022+
"isUpdated": {
1023+
"type": "boolean",
1024+
"readOnly": true
10201025
}
10211026
},
10221027
"additionalProperties": false

test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
10-
using Microsoft.OpenApi.Models;
1110
using Microsoft.Extensions.Options;
11+
using Microsoft.OpenApi.Models;
1212
using Newtonsoft.Json;
1313
using Newtonsoft.Json.Converters;
1414
using Newtonsoft.Json.Linq;
1515
using Newtonsoft.Json.Serialization;
1616
using Swashbuckle.AspNetCore.SwaggerGen;
17+
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;
1718
using Swashbuckle.AspNetCore.TestSupport;
1819
using Xunit;
19-
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;
2020

2121
namespace Swashbuckle.AspNetCore.Newtonsoft.Test
2222
{
@@ -340,7 +340,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt
340340
Assert.False(schema.Properties["StringWithRequired"].Nullable);
341341
Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable);
342342
Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength);
343-
Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray());
343+
Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required);
344+
Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description);
345+
Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly);
344346
}
345347

346348
[Fact]
@@ -736,7 +738,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonIgnore()
736738
var referenceSchema = Subject().GenerateSchema(typeof(JsonIgnoreAnnotatedType), schemaRepository);
737739

738740
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
739-
Assert.Equal(new[] { /* "StringWithJsonIgnore" */ "StringWithNoAnnotation" }, schema.Properties.Keys.ToArray());
741+
Assert.Equal(["StringWithNoAnnotation"], schema.Properties.Keys);
740742
}
741743

742744
[Fact]
@@ -748,8 +750,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty()
748750

749751
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
750752
Assert.Equal(
751-
new[]
752-
{
753+
[
753754
"string-with-json-property-name",
754755
"IntWithRequiredDefault",
755756
"StringWithRequiredDefault",
@@ -758,17 +759,16 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty()
758759
"StringWithRequiredAllowNull",
759760
"StringWithRequiredAlwaysButConflictingDataMember",
760761
"StringWithRequiredDefaultButConflictingDataMember"
761-
},
762-
schema.Properties.Keys.ToArray()
762+
],
763+
schema.Properties.Keys
763764
);
764765
Assert.Equal(
765-
new[]
766-
{
766+
[
767767
"StringWithRequiredAllowNull",
768768
"StringWithRequiredAlways",
769769
"StringWithRequiredAlwaysButConflictingDataMember"
770-
},
771-
schema.Required.ToArray()
770+
],
771+
schema.Required
772772
);
773773
Assert.True(schema.Properties["string-with-json-property-name"].Nullable);
774774
Assert.False(schema.Properties["IntWithRequiredDefault"].Nullable);
@@ -788,7 +788,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired()
788788
var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository);
789789

790790
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
791-
Assert.Equal(new[] { "StringWithConflictingRequired", "StringWithJsonRequired"}, schema.Required.ToArray());
791+
Assert.Equal(["StringWithConflictingRequired", "StringWithJsonRequired"], schema.Required);
792792
Assert.False(schema.Properties["StringWithJsonRequired"].Nullable);
793793
}
794794

@@ -801,14 +801,13 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonObject()
801801

802802
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
803803
Assert.Equal(
804-
new[]
805-
{
804+
[
806805
"StringWithDataMemberRequiredFalse",
807806
"StringWithNoAnnotation",
808807
"StringWithRequiredAllowNull",
809808
"StringWithRequiredUnspecified"
810-
},
811-
schema.Required.ToArray()
809+
],
810+
schema.Required
812811
);
813812
Assert.False(schema.Properties["StringWithNoAnnotation"].Nullable);
814813
Assert.False(schema.Properties["StringWithRequiredUnspecified"].Nullable);
@@ -861,24 +860,22 @@ public void GenerateSchema_HonorsDataMemberAttribute()
861860
Assert.True(schema.Properties["NonRequiredWithCustomNameFromDataMember"].Nullable);
862861

863862
Assert.Equal(
864-
new[]
865-
{
863+
[
866864

867865
"StringWithDataMemberRequired",
868866
"StringWithDataMemberNonRequired",
869867
"RequiredWithCustomNameFromDataMember",
870868
"NonRequiredWithCustomNameFromDataMember"
871-
},
872-
schema.Properties.Keys.ToArray()
869+
],
870+
schema.Properties.Keys
873871
);
874872

875873
Assert.Equal(
876-
new[]
877-
{
874+
[
878875
"RequiredWithCustomNameFromDataMember",
879876
"StringWithDataMemberRequired"
880-
},
881-
schema.Required.ToArray()
877+
],
878+
schema.Required
882879
);
883880
}
884881

test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt
364364
Assert.False(schema.Properties["StringWithRequired"].Nullable);
365365
Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable);
366366
Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength);
367-
Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray());
367+
Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required);
368+
Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description);
369+
Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly);
368370
}
369371

370372
[Fact]
@@ -420,7 +422,7 @@ public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidatio
420422
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
421423
Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength);
422424
Assert.True(schema.Properties["RequiredProperty"].Nullable);
423-
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
425+
Assert.Equal(["RequiredProperty"], schema.Required);
424426
}
425427

426428
#nullable enable
@@ -1153,14 +1155,14 @@ public void GenerateSchema_HonorsSerializerAttributes_JsonIgnore()
11531155
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
11541156

11551157
string[] expectedKeys =
1156-
{
1158+
[
11571159
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionNever),
11581160
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingDefault),
11591161
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingNull),
11601162
nameof(JsonIgnoreAnnotatedType.StringWithNoAnnotation)
1161-
};
1163+
];
11621164

1163-
Assert.Equal(expectedKeys, schema.Properties.Keys.ToArray());
1165+
Assert.Equal(expectedKeys, schema.Properties.Keys);
11641166
}
11651167

11661168
[Fact]
@@ -1171,7 +1173,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonPropertyName()
11711173
var referenceSchema = Subject().GenerateSchema(typeof(JsonPropertyNameAnnotatedType), schemaRepository);
11721174

11731175
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
1174-
Assert.Equal(new[] { "string-with-json-property-name" }, schema.Properties.Keys.ToArray());
1176+
Assert.Equal(["string-with-json-property-name"], schema.Properties.Keys);
11751177
}
11761178

11771179
#if NET7_0_OR_GREATER
@@ -1183,7 +1185,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired()
11831185
var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository);
11841186

11851187
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
1186-
Assert.Equal(new[] { "StringWithJsonRequired" }, schema.Required.ToArray());
1188+
Assert.Equal(["StringWithJsonRequired"], schema.Required);
11871189
Assert.True(schema.Properties["StringWithJsonRequired"].Nullable);
11881190
}
11891191
#endif

test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
23

34
namespace Swashbuckle.AspNetCore.TestSupport
45
{
@@ -43,5 +44,11 @@ public class TypeWithValidationAttributes
4344

4445
[Required(AllowEmptyStrings = true)]
4546
public string StringWithRequiredAllowEmptyTrue { get; set; }
47+
48+
[Description("Description")]
49+
public string StringWithDescription { get; set; }
50+
51+
[ReadOnly(true)]
52+
public string StringWithReadOnly { get; set; }
4653
}
4754
}

test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
23
using Microsoft.AspNetCore.Mvc;
34

45
namespace Swashbuckle.AspNetCore.TestSupport
@@ -33,6 +34,10 @@ public class TypeWithValidationAttributesViaMetadataType
3334
public string StringWithRequired { get; set; }
3435

3536
public string StringWithRequiredAllowEmptyTrue { get; set; }
37+
38+
public string StringWithDescription { get; set; }
39+
40+
public string StringWithReadOnly { get; set; }
3641
}
3742

3843
public class MetadataType
@@ -76,5 +81,11 @@ public class MetadataType
7681

7782
[Required(AllowEmptyStrings = true)]
7883
public string StringWithRequiredAllowEmptyTrue { get; set; }
84+
85+
[Description("Description")]
86+
public string StringWithDescription { get; set; }
87+
88+
[ReadOnly(true)]
89+
public string StringWithReadOnly { get; set; }
7990
}
8091
}

test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.Serialization;
1+
using System.ComponentModel;
2+
using System.Text.Json.Serialization;
23
using Microsoft.AspNetCore.Mvc;
34

45
namespace WebApi.EndPoints
@@ -95,8 +96,8 @@ record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
9596
record class Person(string FirstName, string LastName);
9697

9798
record class Address(string Street, string City, string State, string ZipCode);
98-
sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates);
99-
sealed record CurrenciesRate([property: JsonRequired] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate);
99+
sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates, [property: ReadOnly(true)] bool IsUpdated);
100+
sealed record CurrenciesRate([property: JsonRequired, Description("Currency From")] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate);
100101
record TypeWithTryParse(string Name)
101102
{
102103
public static bool TryParse(string value, out TypeWithTryParse? result)

0 commit comments

Comments
 (0)