Skip to content

Always ref request body and response schemas and fix nested types #56513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ internal static class JsonTypeInfoExtensions
internal static string? GetSchemaReferenceId(this JsonTypeInfo jsonTypeInfo, bool isTopLevel = true)
{
var type = jsonTypeInfo.Type;
if (isTopLevel && OpenApiConstants.PrimitiveTypes.Contains(type))
var underlyingType = Nullable.GetUnderlyingType(type);
if (isTopLevel && OpenApiConstants.PrimitiveTypes.Contains(underlyingType ?? type))
{
return null;
}
Expand Down
4 changes: 4 additions & 0 deletions src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
schema.Enum = [ReadOpenApiAny(ref reader, out var constType)];
schema.Type = constType;
break;
case OpenApiSchemaKeywords.RefKeyword:
reader.Read();
schema.Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = reader.GetString() };
break;
default:
reader.Skip();
break;
Expand Down
4 changes: 2 additions & 2 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ private async Task<OpenApiResponse> GetResponseAsync(ApiDescription apiDescripti
.Select(responseFormat => responseFormat.MediaType);
foreach (var contentType in apiResponseFormatContentTypes)
{
var schema = apiResponseType.Type is { } type ? await _componentService.GetOrCreateSchemaAsync(type, null, cancellationToken) : new OpenApiSchema();
var schema = apiResponseType.Type is { } type ? await _componentService.GetOrCreateSchemaAsync(type, null, cancellationToken, captureSchemaByRef: true) : new OpenApiSchema();
response.Content[contentType] = new OpenApiMediaType { Schema = schema };
}

Expand Down Expand Up @@ -465,7 +465,7 @@ private async Task<OpenApiRequestBody> GetJsonRequestBody(IList<ApiRequestFormat
foreach (var requestForm in supportedRequestFormats)
{
var contentType = requestForm.MediaType;
requestBody.Content[contentType] = new OpenApiMediaType { Schema = await _componentService.GetOrCreateSchemaAsync(bodyParameter.Type, bodyParameter, cancellationToken) };
requestBody.Content[contentType] = new OpenApiMediaType { Schema = await _componentService.GetOrCreateSchemaAsync(bodyParameter.Type, bodyParameter, cancellationToken, captureSchemaByRef: true) };
}

return requestBody;
Expand Down
12 changes: 10 additions & 2 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ internal sealed class OpenApiSchemaService(
schema.ApplyPolymorphismOptions(context);
if (context.PropertyInfo is { AttributeProvider: { } attributeProvider } jsonPropertyInfo)
{
// Short-circuit STJ's handling of nested properties, which uses a reference to the
// properties type schema with a schema that uses a document level reference.
// For example, if the property is a `public NestedTyped Nested { get; set; }` property,
// "nested": "#/properties/nested" because "nested": "#/components/schemas/NestedType"
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType)
{
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = context.TypeInfo.GetSchemaReferenceId() };
}
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ValidationAttribute>() is { } validationAttributes)
{
Expand All @@ -112,7 +120,7 @@ internal sealed class OpenApiSchemaService(
}
};

internal async Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
internal async Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default, bool captureSchemaByRef = false)
{
var key = parameterDescription?.ParameterDescriptor is IParameterInfoParameterDescriptor parameterInfoDescription
&& parameterDescription.ModelMetadata.PropertyName is null
Expand All @@ -126,7 +134,7 @@ internal async Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParamete
Debug.Assert(deserializedSchema != null, "The schema should have been deserialized successfully and materialize a non-null value.");
var schema = deserializedSchema.Schema;
await ApplySchemaTransformersAsync(schema, type, parameterDescription, cancellationToken);
_schemaStore.PopulateSchemaIntoReferenceCache(schema);
_schemaStore.PopulateSchemaIntoReferenceCache(schema, captureSchemaByRef);
return schema;
}

Expand Down
11 changes: 7 additions & 4 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ public JsonNode GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonNode>
/// schemas into the top-level document.
/// </summary>
/// <param name="schema">The <see cref="OpenApiSchema"/> to add to the schemas-with-references cache.</param>
public void PopulateSchemaIntoReferenceCache(OpenApiSchema schema)
/// <param name="captureSchemaByRef"><see langword="true"/> if schema should always be referenced instead of inlined.</param>
public void PopulateSchemaIntoReferenceCache(OpenApiSchema schema, bool captureSchemaByRef)
{
AddOrUpdateSchemaByReference(schema);
// Only capture top-level schemas by ref. Nested schemas will follow the
// reference by duplicate rules.
AddOrUpdateSchemaByReference(schema, captureSchemaByRef: captureSchemaByRef);
if (schema.AdditionalProperties is not null)
{
AddOrUpdateSchemaByReference(schema.AdditionalProperties);
Expand Down Expand Up @@ -119,10 +122,10 @@ public void PopulateSchemaIntoReferenceCache(OpenApiSchema schema)
}
}

private void AddOrUpdateSchemaByReference(OpenApiSchema schema, string? baseTypeSchemaId = null)
private void AddOrUpdateSchemaByReference(OpenApiSchema schema, string? baseTypeSchemaId = null, bool captureSchemaByRef = false)
{
var targetReferenceId = baseTypeSchemaId is not null ? $"{baseTypeSchemaId}{GetSchemaReferenceId(schema)}" : GetSchemaReferenceId(schema);
if (SchemasByReference.TryGetValue(schema, out var referenceId))
if (SchemasByReference.TryGetValue(schema, out var referenceId) || captureSchemaByRef)
{
// If we've already used this reference ID else where in the document, increment a counter value to the reference
// ID to avoid name collisions. These collisions are most likely to occur when the same .NET type produces a different
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,7 @@
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"hypotenuse": {
"type": "number",
"format": "double"
},
"color": {
"type": "string"
},
"sides": {
"type": "integer",
"format": "int32"
}
}
"$ref": "#/components/schemas/Triangle"
}
}
}
Expand All @@ -91,25 +78,7 @@
"content": {
"application/json": {
"schema": {
"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"
}
}
"$ref": "#/components/schemas/Shape"
}
}
}
Expand All @@ -120,6 +89,27 @@
},
"components": {
"schemas": {
"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": {
Expand Down Expand Up @@ -186,6 +176,22 @@
"format": "date-time"
}
}
},
"Triangle": {
"type": "object",
"properties": {
"hypotenuse": {
"type": "number",
"format": "double"
},
"color": {
"type": "string"
},
"sides": {
"type": "integer",
"format": "int32"
}
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,25 +303,7 @@
"content": {
"application/json": {
"schema": {
"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"
}
}
"$ref": "#/components/schemas/Shape"
}
}
},
Expand All @@ -343,29 +325,7 @@
"content": {
"application/json": {
"schema": {
"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"
}
}
"$ref": "#/components/schemas/WeatherForecastBase"
}
}
},
Expand All @@ -387,25 +347,7 @@
"content": {
"application/json": {
"schema": {
"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"
}
}
"$ref": "#/components/schemas/Person"
}
}
},
Expand Down Expand Up @@ -447,6 +389,27 @@
"format": "int32"
}
},
"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": {
Expand Down Expand Up @@ -489,6 +452,27 @@
}
}
},
"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": {
Expand Down Expand Up @@ -547,6 +531,31 @@
}
}
},
"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"
Expand Down
Loading