Skip to content

Commit

Permalink
Support [Description] and [ReadOnly] (#3162)
Browse files Browse the repository at this point in the history
Support `[Description]` and `[ReadOnly]` attributes.
  • Loading branch information
jgarciadelanoceda authored Nov 22, 2024
1 parent e727e8d commit bb49884
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing.Constraints;
Expand Down Expand Up @@ -60,6 +61,13 @@ public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumera

else if (attribute is StringLengthAttribute stringLengthAttribute)
ApplyStringLengthAttribute(schema, stringLengthAttribute);

else if (attribute is ReadOnlyAttribute readOnlyAttribute)
ApplyReadOnlyAttribute(schema, readOnlyAttribute);

else if (attribute is DescriptionAttribute descriptionAttribute)
ApplyDescriptionAttribute(schema, descriptionAttribute);

}
}

Expand Down Expand Up @@ -230,6 +238,16 @@ private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengt
schema.MaxLength = stringLengthAttribute.MaximumLength;
}

private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribute readOnlyAttribute)
{
schema.ReadOnly = readOnlyAttribute.IsReadOnly;
}

private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute)
{
schema.Description ??= descriptionAttribute.Description;
}

private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint)
{
schema.MinLength = lengthRouteConstraint.MinLength;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@
"properties": {
"currencyFrom": {
"type": "string",
"description": "Currency From",
"nullable": true
},
"currencyTo": {
Expand Down Expand Up @@ -1017,6 +1018,10 @@
"$ref": "#/components/schemas/CurrenciesRate"
},
"nullable": true
},
"isUpdated": {
"type": "boolean",
"readOnly": true
}
},
"additionalProperties": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@
"properties": {
"currencyFrom": {
"type": "string",
"description": "Currency From",
"nullable": true
},
"currencyTo": {
Expand Down Expand Up @@ -1017,6 +1018,10 @@
"$ref": "#/components/schemas/CurrenciesRate"
},
"nullable": true
},
"isUpdated": {
"type": "boolean",
"readOnly": true
}
},
"additionalProperties": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;
using Swashbuckle.AspNetCore.TestSupport;
using Xunit;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;

namespace Swashbuckle.AspNetCore.Newtonsoft.Test
{
Expand Down Expand Up @@ -340,7 +340,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt
Assert.False(schema.Properties["StringWithRequired"].Nullable);
Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable);
Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength);
Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray());
Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required);
Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description);
Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly);
}

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

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

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

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(
new[]
{
[
"string-with-json-property-name",
"IntWithRequiredDefault",
"StringWithRequiredDefault",
Expand All @@ -758,17 +759,16 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonProperty()
"StringWithRequiredAllowNull",
"StringWithRequiredAlwaysButConflictingDataMember",
"StringWithRequiredDefaultButConflictingDataMember"
},
schema.Properties.Keys.ToArray()
],
schema.Properties.Keys
);
Assert.Equal(
new[]
{
[
"StringWithRequiredAllowNull",
"StringWithRequiredAlways",
"StringWithRequiredAlwaysButConflictingDataMember"
},
schema.Required.ToArray()
],
schema.Required
);
Assert.True(schema.Properties["string-with-json-property-name"].Nullable);
Assert.False(schema.Properties["IntWithRequiredDefault"].Nullable);
Expand All @@ -788,7 +788,7 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonRequired()
var referenceSchema = Subject().GenerateSchema(typeof(JsonRequiredAnnotatedType), schemaRepository);

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

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

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(
new[]
{
[
"StringWithDataMemberRequiredFalse",
"StringWithNoAnnotation",
"StringWithRequiredAllowNull",
"StringWithRequiredUnspecified"
},
schema.Required.ToArray()
],
schema.Required
);
Assert.False(schema.Properties["StringWithNoAnnotation"].Nullable);
Assert.False(schema.Properties["StringWithRequiredUnspecified"].Nullable);
Expand Down Expand Up @@ -861,24 +860,22 @@ public void GenerateSchema_HonorsDataMemberAttribute()
Assert.True(schema.Properties["NonRequiredWithCustomNameFromDataMember"].Nullable);

Assert.Equal(
new[]
{
[

"StringWithDataMemberRequired",
"StringWithDataMemberNonRequired",
"RequiredWithCustomNameFromDataMember",
"NonRequiredWithCustomNameFromDataMember"
},
schema.Properties.Keys.ToArray()
],
schema.Properties.Keys
);

Assert.Equal(
new[]
{
[
"RequiredWithCustomNameFromDataMember",
"StringWithDataMemberRequired"
},
schema.Required.ToArray()
],
schema.Required
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt
Assert.False(schema.Properties["StringWithRequired"].Nullable);
Assert.False(schema.Properties["StringWithRequiredAllowEmptyTrue"].Nullable);
Assert.Null(schema.Properties["StringWithRequiredAllowEmptyTrue"].MinLength);
Assert.Equal(new[] { "StringWithRequired", "StringWithRequiredAllowEmptyTrue" }, schema.Required.ToArray());
Assert.Equal(["StringWithRequired", "StringWithRequiredAllowEmptyTrue"], schema.Required);
Assert.Equal("Description", schema.Properties[nameof(TypeWithValidationAttributes.StringWithDescription)].Description);
Assert.True(schema.Properties[nameof(TypeWithValidationAttributes.StringWithReadOnly)].ReadOnly);
}

[Fact]
Expand Down Expand Up @@ -420,7 +422,7 @@ public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidatio
var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength);
Assert.True(schema.Properties["RequiredProperty"].Nullable);
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
Assert.Equal(["RequiredProperty"], schema.Required);
}

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

string[] expectedKeys =
{
[
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionNever),
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingDefault),
nameof(JsonIgnoreAnnotatedType.StringWithJsonIgnoreConditionWhenWritingNull),
nameof(JsonIgnoreAnnotatedType.StringWithNoAnnotation)
};
];

Assert.Equal(expectedKeys, schema.Properties.Keys.ToArray());
Assert.Equal(expectedKeys, schema.Properties.Keys);
}

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

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

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

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(new[] { "StringWithJsonRequired" }, schema.Required.ToArray());
Assert.Equal(["StringWithJsonRequired"], schema.Required);
Assert.True(schema.Properties["StringWithJsonRequired"].Nullable);
}
#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Swashbuckle.AspNetCore.TestSupport
{
Expand Down Expand Up @@ -43,5 +44,11 @@ public class TypeWithValidationAttributes

[Required(AllowEmptyStrings = true)]
public string StringWithRequiredAllowEmptyTrue { get; set; }

[Description("Description")]
public string StringWithDescription { get; set; }

[ReadOnly(true)]
public string StringWithReadOnly { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

namespace Swashbuckle.AspNetCore.TestSupport
Expand Down Expand Up @@ -33,6 +34,10 @@ public class TypeWithValidationAttributesViaMetadataType
public string StringWithRequired { get; set; }

public string StringWithRequiredAllowEmptyTrue { get; set; }

public string StringWithDescription { get; set; }

public string StringWithReadOnly { get; set; }
}

public class MetadataType
Expand Down Expand Up @@ -76,5 +81,11 @@ public class MetadataType

[Required(AllowEmptyStrings = true)]
public string StringWithRequiredAllowEmptyTrue { get; set; }

[Description("Description")]
public string StringWithDescription { get; set; }

[ReadOnly(true)]
public string StringWithReadOnly { get; set; }
}
}
7 changes: 4 additions & 3 deletions test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.ComponentModel;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;

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

record class Address(string Street, string City, string State, string ZipCode);
sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates);
sealed record CurrenciesRate([property: JsonRequired] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate);
sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates, [property: ReadOnly(true)] bool IsUpdated);
sealed record CurrenciesRate([property: JsonRequired, Description("Currency From")] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate);
record TypeWithTryParse(string Name)
{
public static bool TryParse(string value, out TypeWithTryParse? result)
Expand Down

0 comments on commit bb49884

Please sign in to comment.