diff --git a/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs b/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs index 205db25ecd4e..ee2148f40481 100644 --- a/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs +++ b/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs @@ -39,6 +39,7 @@ public static IEndpointRouteBuilder MapSchemasEndpoints(this IEndpointRouteBuild schemas.MapPost("/child", (ChildObject child) => Results.Ok(child)); schemas.MapPatch("/json-patch", (JsonPatchDocument patchDoc) => Results.NoContent()); schemas.MapPatch("/json-patch-generic", (JsonPatchDocument patchDoc) => Results.NoContent()); + schemas.MapPost("/descriptions", ([Description("Described Parameter")] DescribedDto describedDto) => Results.NoContent()); return endpointRouteBuilder; } @@ -101,4 +102,17 @@ public sealed class ChildObject public int Id { get; set; } public required ParentObject Parent { get; set; } } + + [Description("Type Description")] + public sealed class DescribedDto + { + [Description("Property Description on string")] + public string DescribedString { get; set; } + + [Description("Property Description on Referenced Object")] + public Tag ReferencedTag { get; set; } + + [Description("Property Description on Second Referenced Object")] + public Tag SecondReferencedTag { get; set; } + } } diff --git a/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs b/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs index fa34c306ef67..ab00e10ee6e9 100644 --- a/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs +++ b/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs @@ -19,6 +19,8 @@ public static IEndpointRouteBuilder MapXmlEndpoints(this IEndpointRouteBuilder e group.MapPost("/todo-with-description", (TodoWithDescription todo) => { }); + group.MapGet("/list", () => TypedResults.Ok(new TodoList())); + return endpointRouteBuilder; } @@ -111,4 +113,25 @@ public class TodoWithDescription : ITodo /// Another description of the todo. public required string Description { get; set; } } + + /// + /// TodoList to show references. + /// + public class TodoList + { + /// + /// A description of the list of Todo Items. + /// + public List Items { get; set; } = []; + + /// + /// Last completed Todo. + /// + public TodoWithDescription LastCompleted { get; set; } = null!; + + /// + /// First completed Todo. + /// + public TodoWithDescription FirstCompleted { get; set; } = null!; + } } diff --git a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index 0b660ceb1d66..22a19c2d0999 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -345,6 +345,17 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, schema.Metadata ??= new Dictionary(); schema.Metadata[OpenApiConstants.RefId] = reader.GetString() ?? string.Empty; break; + case OpenApiConstants.RefDescriptionAnnotation: + reader.Read(); + schema.Metadata ??= new Dictionary(); + schema.Metadata[OpenApiConstants.RefDescriptionAnnotation] = reader.GetString() ?? string.Empty; + break; + case OpenApiConstants.RefExampleAnnotation: + reader.Read(); + schema.Metadata ??= new Dictionary(); + schema.Metadata[OpenApiConstants.RefExampleAnnotation] = reader.GetString() ?? string.Empty; + break; + default: reader.Skip(); break; diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 396a3b76c4be..e3d0999414ac 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -101,20 +101,34 @@ internal sealed class OpenApiSchemaService( { schema.ApplyNullabilityContextInfo(jsonPropertyInfo); } - var isInlinedSchema = schema["x-schema-id"] is null; - if (isInlinedSchema && context.PropertyInfo is { AttributeProvider: { } attributeProvider }) + if (context.TypeInfo.Type.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DescriptionAttribute typeDescriptionAttribute) { - if (attributeProvider.GetCustomAttributes(inherit: false).OfType() is { } validationAttributes) - { - schema.ApplyValidationAttributes(validationAttributes); - } - if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DefaultValueAttribute defaultValueAttribute) + schema[OpenApiSchemaKeywords.DescriptionKeyword] = typeDescriptionAttribute.Description; + } + if (context.PropertyInfo is { AttributeProvider: { } attributeProvider }) + { + var isInlinedSchema = schema["x-schema-id"] is null; + if (isInlinedSchema) { - schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo); + if (attributeProvider.GetCustomAttributes(inherit: false).OfType() is { } validationAttributes) + { + schema.ApplyValidationAttributes(validationAttributes); + } + if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DefaultValueAttribute defaultValueAttribute) + { + schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo); + } + if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DescriptionAttribute descriptionAttribute) + { + schema[OpenApiSchemaKeywords.DescriptionKeyword] = descriptionAttribute.Description; + } } - if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DescriptionAttribute descriptionAttribute) + else { - schema[OpenApiSchemaKeywords.DescriptionKeyword] = descriptionAttribute.Description; + if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DescriptionAttribute descriptionAttribute) + { + schema[OpenApiConstants.RefDescriptionAnnotation] = descriptionAttribute.Description; + } } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt new file mode 100644 index 000000000000..97a02e4c80f6 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt @@ -0,0 +1,1047 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Sample | schemas-by-ref", + "version": "1.0.0" + }, + "paths": { + "/schemas-by-ref/typed-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/multiple-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/schemas-by-ref/iresult-no-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/iresult-with-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/xml": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/primitives": { + "get": { + "tags": [ + "Sample" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The ID associated with the Todo item.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "size", + "in": "query", + "description": "The number of Todos to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/product": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "/schemas-by-ref/account": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + } + } + } + }, + "/schemas-by-ref/array-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/list-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/ienumerable-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/frozen-dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/shape": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shape" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/weatherforecastbase": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeatherForecastBase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/person": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/category": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/container": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerType" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/root": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Root" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/location": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationContainer" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/parent": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/child": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChildObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/json-patch": { + "patch": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/JsonPatchDocument" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/json-patch-generic": { + "patch": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/JsonPatchDocument" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/descriptions": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "description": "Described Parameter", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DescribedDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "AddressDto": { + "required": [ + "relatedLocation" + ], + "type": "object", + "properties": { + "relatedLocation": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "Category": { + "required": [ + "name", + "parent" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/Category" + }, + "tags": { + "$ref": "#/components/schemas/Category/properties/parent/properties/tags" + } + } + }, + "ChildObject": { + "required": [ + "parent" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "parent": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "ContainerType": { + "type": "object", + "properties": { + "seq1": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "seq2": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContainerType/properties/seq1/items" + } + } + } + }, + "DescribedDto": { + "type": "object", + "properties": { + "describedString": { + "type": "string", + "description": "Property Description on string" + }, + "referencedTag": { + "$ref": "#/components/schemas/Tag" + }, + "secondReferencedTag": { + "$ref": "#/components/schemas/Tag" + } + } + }, + "Item": { + "type": "object", + "properties": { + "name": { + "$ref": "#/components/schemas/Root/properties/item1/properties/name" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "JsonPatchDocument": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "required": [ + "op", + "path", + "value" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "add", + "replace", + "test" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "value": { } + }, + "additionalProperties": false + }, + { + "required": [ + "op", + "path", + "from" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "move", + "copy" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "from": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "required": [ + "op", + "path" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "remove" + ], + "type": "string" + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + }, + "LocationContainer": { + "required": [ + "location" + ], + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "LocationDto": { + "required": [ + "address" + ], + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/AddressDto" + } + } + }, + "ParentObject": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChildObject" + } + } + } + }, + "Person": { + "required": [ + "discriminator" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/PersonStudent" + }, + { + "$ref": "#/components/schemas/PersonTeacher" + } + ], + "discriminator": { + "propertyName": "discriminator", + "mapping": { + "student": "#/components/schemas/PersonStudent", + "teacher": "#/components/schemas/PersonTeacher" + } + } + }, + "PersonStudent": { + "properties": { + "discriminator": { + "enum": [ + "student" + ], + "type": "string" + }, + "gpa": { + "type": "number", + "format": "double" + } + } + }, + "PersonTeacher": { + "required": [ + "subject" + ], + "properties": { + "discriminator": { + "enum": [ + "teacher" + ], + "type": "string" + }, + "subject": { + "type": "string" + } + } + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "Root": { + "type": "object", + "properties": { + "item1": { + "$ref": "#/components/schemas/Item" + }, + "item2": { + "$ref": "#/components/schemas/Item" + } + } + }, + "Shape": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/ShapeTriangle" + }, + { + "$ref": "#/components/schemas/ShapeSquare" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "triangle": "#/components/schemas/ShapeTriangle", + "square": "#/components/schemas/ShapeSquare" + } + } + }, + "ShapeSquare": { + "properties": { + "$type": { + "enum": [ + "square" + ], + "type": "string" + }, + "area": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "ShapeTriangle": { + "properties": { + "$type": { + "enum": [ + "triangle" + ], + "type": "string" + }, + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "Tag": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "Triangle": { + "type": "object", + "properties": { + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "WeatherForecastBase": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "0": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity", + "1": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries", + "2": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + } + }, + "WeatherForecastBaseWeatherForecastWithCity": { + "required": [ + "city" + ], + "properties": { + "$type": { + "enum": [ + 0 + ], + "type": "integer" + }, + "city": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithLocalNews": { + "required": [ + "news" + ], + "properties": { + "$type": { + "enum": [ + 2 + ], + "type": "integer" + }, + "news": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithTimeSeries": { + "required": [ + "summary" + ], + "properties": { + "$type": { + "enum": [ + 1 + ], + "type": "integer" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] +} \ No newline at end of file diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt new file mode 100644 index 000000000000..318c5ff5b2e1 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt @@ -0,0 +1,435 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Sample | xml", + "version": "1.0.0" + }, + "paths": { + "/xml/type-with-examples": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + } + } + } + } + }, + "/xml/todo": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoFomInterface" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/board": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardItem" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project-record": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRecord" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/todo-with-description": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoWithDescription" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/list": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoList" + } + } + } + } + } + } + }, + "/Xml": { + "get": { + "tags": [ + "Xml" + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the person.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns the greeting.", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Xml" + ], + "requestBody": { + "description": "The todo to insert into the database.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BoardItem": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "description": "An item on the board." + }, + "Project": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "description": "The project that contains Todo items." + }, + "ProjectRecord": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string", + "description": "The description of the project." + } + }, + "description": "The project that contains Todo items." + }, + "Todo": { + "required": [ + "id", + "title", + "completed" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string" + }, + "completed": { + "type": "boolean" + } + } + }, + "TodoFomInterface": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo." + }, + "description": { + "type": "string", + "description": "A description of the todo." + } + }, + "description": "This is a todo item." + }, + "TodoList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TodoWithDescription" + }, + "description": "A description of the list of Todo Items." + }, + "lastCompleted": { + "$ref": "#/components/schemas/TodoWithDescription" + }, + "firstCompleted": { + "$ref": "#/components/schemas/TodoWithDescription" + } + }, + "description": "TodoList to show references." + }, + "TodoWithDescription": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo, overridden.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo, overridden." + }, + "description": { + "type": "string", + "description": "Another description of the todo." + } + } + }, + "TypeWithExamples": { + "type": "object", + "properties": { + "booleanType": { + "type": "boolean", + "example": true + }, + "integerType": { + "type": "integer", + "format": "int32", + "example": 42 + }, + "longType": { + "type": "integer", + "format": "int64", + "example": 1234567890123456789 + }, + "doubleType": { + "type": "number", + "format": "double", + "example": 3.14 + }, + "floatType": { + "type": "number", + "format": "float", + "example": 3.14 + }, + "dateTimeType": { + "type": "string", + "format": "date-time", + "example": "2022-01-01T00:00:00Z" + }, + "dateOnlyType": { + "type": "string", + "format": "date", + "example": "2022-01-01" + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + }, + { + "name": "Xml" + } + ] +} \ No newline at end of file diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt new file mode 100644 index 000000000000..0555831817f5 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.received.txt @@ -0,0 +1,1049 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Sample | schemas-by-ref", + "version": "1.0.0" + }, + "paths": { + "/schemas-by-ref/typed-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/multiple-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/schemas-by-ref/iresult-no-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/iresult-with-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/xml": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/primitives": { + "get": { + "tags": [ + "Sample" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The ID associated with the Todo item.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "size", + "in": "query", + "description": "The number of Todos to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/product": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "/schemas-by-ref/account": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + } + } + } + }, + "/schemas-by-ref/array-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/list-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/ienumerable-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/frozen-dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/shape": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shape" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/weatherforecastbase": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeatherForecastBase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/person": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/category": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/container": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerType" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/root": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Root" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/location": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationContainer" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/parent": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/child": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChildObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/json-patch": { + "patch": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/JsonPatchDocument" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/json-patch-generic": { + "patch": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/JsonPatchDocument" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/descriptions": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "description": "Described Parameter", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DescribedDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "AddressDto": { + "required": [ + "relatedLocation" + ], + "type": "object", + "properties": { + "relatedLocation": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "Category": { + "required": [ + "name", + "parent" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/Category" + }, + "tags": { + "$ref": "#/components/schemas/Category/properties/parent/properties/tags" + } + } + }, + "ChildObject": { + "required": [ + "parent" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "parent": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "ContainerType": { + "type": "object", + "properties": { + "seq1": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "seq2": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContainerType/properties/seq1/items" + } + } + } + }, + "DescribedDto": { + "type": "object", + "properties": { + "describedString": { + "type": "string", + "description": "Property Description on string" + }, + "referencedTag": { + "description": "Property Description on Referenced Object", + "$ref": "#/components/schemas/Tag" + }, + "secondReferencedTag": { + "description": "Property Description on Second Referenced Object", + "$ref": "#/components/schemas/Tag" + } + } + }, + "Item": { + "type": "object", + "properties": { + "name": { + "$ref": "#/components/schemas/Root/properties/item1/properties/name" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "JsonPatchDocument": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "required": [ + "op", + "path", + "value" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "add", + "replace", + "test" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "value": { } + }, + "additionalProperties": false + }, + { + "required": [ + "op", + "path", + "from" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "move", + "copy" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "from": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "required": [ + "op", + "path" + ], + "type": "object", + "properties": { + "op": { + "enum": [ + "remove" + ], + "type": "string" + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + }, + "LocationContainer": { + "required": [ + "location" + ], + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "LocationDto": { + "required": [ + "address" + ], + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/AddressDto" + } + } + }, + "ParentObject": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChildObject" + } + } + } + }, + "Person": { + "required": [ + "discriminator" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/PersonStudent" + }, + { + "$ref": "#/components/schemas/PersonTeacher" + } + ], + "discriminator": { + "propertyName": "discriminator", + "mapping": { + "student": "#/components/schemas/PersonStudent", + "teacher": "#/components/schemas/PersonTeacher" + } + } + }, + "PersonStudent": { + "properties": { + "discriminator": { + "enum": [ + "student" + ], + "type": "string" + }, + "gpa": { + "type": "number", + "format": "double" + } + } + }, + "PersonTeacher": { + "required": [ + "subject" + ], + "properties": { + "discriminator": { + "enum": [ + "teacher" + ], + "type": "string" + }, + "subject": { + "type": "string" + } + } + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "Root": { + "type": "object", + "properties": { + "item1": { + "$ref": "#/components/schemas/Item" + }, + "item2": { + "$ref": "#/components/schemas/Item" + } + } + }, + "Shape": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/ShapeTriangle" + }, + { + "$ref": "#/components/schemas/ShapeSquare" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "triangle": "#/components/schemas/ShapeTriangle", + "square": "#/components/schemas/ShapeSquare" + } + } + }, + "ShapeSquare": { + "properties": { + "$type": { + "enum": [ + "square" + ], + "type": "string" + }, + "area": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "ShapeTriangle": { + "properties": { + "$type": { + "enum": [ + "triangle" + ], + "type": "string" + }, + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "Tag": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "Triangle": { + "type": "object", + "properties": { + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "WeatherForecastBase": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "0": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity", + "1": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries", + "2": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + } + }, + "WeatherForecastBaseWeatherForecastWithCity": { + "required": [ + "city" + ], + "properties": { + "$type": { + "enum": [ + 0 + ], + "type": "integer" + }, + "city": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithLocalNews": { + "required": [ + "news" + ], + "properties": { + "$type": { + "enum": [ + 2 + ], + "type": "integer" + }, + "news": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithTimeSeries": { + "required": [ + "summary" + ], + "properties": { + "$type": { + "enum": [ + 1 + ], + "type": "integer" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] +} \ No newline at end of file diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt new file mode 100644 index 000000000000..645c648e2b24 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.received.txt @@ -0,0 +1,437 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Sample | xml", + "version": "1.0.0" + }, + "paths": { + "/xml/type-with-examples": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + } + } + } + } + }, + "/xml/todo": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoFomInterface" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/board": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardItem" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project-record": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRecord" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/todo-with-description": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoWithDescription" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/list": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoList" + } + } + } + } + } + } + }, + "/Xml": { + "get": { + "tags": [ + "Xml" + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the person.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns the greeting.", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Xml" + ], + "requestBody": { + "description": "The todo to insert into the database.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BoardItem": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "description": "An item on the board." + }, + "Project": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "description": "The project that contains Todo items." + }, + "ProjectRecord": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string", + "description": "The description of the project." + } + }, + "description": "The project that contains Todo items." + }, + "Todo": { + "required": [ + "id", + "title", + "completed" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string" + }, + "completed": { + "type": "boolean" + } + } + }, + "TodoFomInterface": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo." + }, + "description": { + "type": "string", + "description": "A description of the todo." + } + }, + "description": "This is a todo item." + }, + "TodoList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TodoWithDescription" + }, + "description": "A description of the list of Todo Items." + }, + "lastCompleted": { + "description": "Last completed Todo.", + "$ref": "#/components/schemas/TodoWithDescription" + }, + "firstCompleted": { + "description": "First completed Todo.", + "$ref": "#/components/schemas/TodoWithDescription" + } + }, + "description": "TodoList to show references." + }, + "TodoWithDescription": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo, overridden.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo, overridden." + }, + "description": { + "type": "string", + "description": "Another description of the todo." + } + } + }, + "TypeWithExamples": { + "type": "object", + "properties": { + "booleanType": { + "type": "boolean", + "example": true + }, + "integerType": { + "type": "integer", + "format": "int32", + "example": 42 + }, + "longType": { + "type": "integer", + "format": "int64", + "example": 1234567890123456789 + }, + "doubleType": { + "type": "number", + "format": "double", + "example": 3.14 + }, + "floatType": { + "type": "number", + "format": "float", + "example": 3.14 + }, + "dateTimeType": { + "type": "string", + "format": "date-time", + "example": "2022-01-01T00:00:00Z" + }, + "dateOnlyType": { + "type": "string", + "format": "date", + "example": "2022-01-01" + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + }, + { + "name": "Xml" + } + ] +} \ No newline at end of file diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.Annotations.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.Annotations.cs new file mode 100644 index 000000000000..f4bb0a5aa4e7 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.Annotations.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Net.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.AspNetCore.Routing; + +public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase +{ + [Fact] + public async Task SchemaDescriptions_HandlesSchemaReferences() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/", (DescribedReferencesDto dto) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + + Assert.NotNull(requestBody); + var content = Assert.Single(requestBody.Content); + Assert.Equal("application/json", content.Key); + Assert.NotNull(content.Value.Schema); + var schema = content.Value.Schema; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("child1", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("Property: DescribedReferencesDto.Child1", reference.Reference.Description); + }, + property => + { + Assert.Equal("child2", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("Property: DescribedReferencesDto.Child2", reference.Reference.Description); + }, + property => + { + Assert.Equal("childNoDescription", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Null(reference.Reference.Description); + }); + + var referencedSchema = document.Components.Schemas["DescribedChildDto"]; + Assert.Equal("Class: DescribedChildDto", referencedSchema.Description); + }); + + } + + [Description("Class: DescribedReferencesDto")] + public class DescribedReferencesDto + { + [Description("Property: DescribedReferencesDto.Child1")] + public DescribedChildDto Child1 { get; set; } + + [Description("Property: DescribedReferencesDto.Child2")] + public DescribedChildDto Child2 { get; set; } + + public DescribedChildDto ChildNoDescription { get; set; } + } + + [Description("Class: DescribedChildDto")] + public class DescribedChildDto + { + [Description("Property: DescribedChildDto.ChildValue")] + public string ChildValue { get; set; } + } + + [Fact] + public async Task SchemaDescriptions_HandlesInlinedSchemas() + { + // Arrange + var builder = CreateBuilder(); + + var options = new OpenApiOptions(); + var originalCreateSchemaReferenceId = options.CreateSchemaReferenceId; + options.CreateSchemaReferenceId = (x) => x.Type == typeof(DescribedInlinedDto) ? null : originalCreateSchemaReferenceId(x); + + // Act + builder.MapPost("/", (DescribedInlinedSchemasDto dto) => { }); + + // Assert + await VerifyOpenApiDocument(builder, options, document => + { + var operation = document.Paths["/"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + + Assert.NotNull(requestBody); + var content = Assert.Single(requestBody.Content); + Assert.Equal("application/json", content.Key); + Assert.NotNull(content.Value.Schema); + var schema = content.Value.Schema; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("inlined1", property.Key); + var inlinedSchema = Assert.IsType(property.Value); + Assert.Equal("Property: DescribedInlinedSchemasDto.Inlined1", inlinedSchema.Description); + }, + property => + { + Assert.Equal("inlined2", property.Key); + var inlinedSchema = Assert.IsType(property.Value); + Assert.Equal("Property: DescribedInlinedSchemasDto.Inlined2", inlinedSchema.Description); + }, + property => + { + Assert.Equal("inlinedNoDescription", property.Key); + var inlinedSchema = Assert.IsType(property.Value); + Assert.Equal("Class: DescribedInlinedDto", inlinedSchema.Description); + }); + }); + } + + [Description("Class: DescribedInlinedSchemasDto")] + public class DescribedInlinedSchemasDto + { + [Description("Property: DescribedInlinedSchemasDto.Inlined1")] + public DescribedInlinedDto Inlined1 { get; set; } + + [Description("Property: DescribedInlinedSchemasDto.Inlined2")] + public DescribedInlinedDto Inlined2 { get; set; } + + public DescribedInlinedDto InlinedNoDescription { get; set; } + } + + [Description("Class: DescribedInlinedDto")] + public class DescribedInlinedDto + { + [Description("Property: DescribedInlinedDto.ChildValue")] + public string ChildValue { get; set; } + } +}