From 6227ab4603d184962ae934a122dbcc12037febb3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 12 Jul 2024 17:00:27 -0700 Subject: [PATCH] Set additionalProperties on schema when [JsonExtensionData] is present --- src/OpenApi/sample/Program.cs | 2 +- .../Services/Schemas/OpenApiSchemaService.cs | 11 ++++ ...t_documentName=schemas-by-ref.verified.txt | 46 +++++++++++++ ...enApiSchemaService.AdditionalProperties.cs | 65 +++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.AdditionalProperties.cs diff --git a/src/OpenApi/sample/Program.cs b/src/OpenApi/sample/Program.cs index b567bb89a563..a42aefe6156f 100644 --- a/src/OpenApi/sample/Program.cs +++ b/src/OpenApi/sample/Program.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Frozen; using System.Collections.Immutable; using System.ComponentModel; using Microsoft.AspNetCore.Http.HttpResults; @@ -111,6 +110,7 @@ schemas.MapPost("/shape", (Shape shape) => { }); schemas.MapPost("/weatherforecastbase", (WeatherForecastBase forecast) => { }); schemas.MapPost("/person", (Person person) => { }); +schemas.MapPost("/problem-details", () => { }).ProducesProblem(statusCode: 500); app.MapControllers(); diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 750037807d54..37876680d4b5 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -88,6 +88,17 @@ internal sealed class OpenApiSchemaService( { schema = new JsonObject(); } + // STJ operates under the assumption that JSON Schema makes around `additionalProperties` being + // true by default for all schemas. OpenAPI v3 takes a different interpretation and assumes that + // `additionalProperties` is false by default. We override the default behavior assumed by STJ to + // match the OpenAPI v3 interpretation here by checking to see if any properties on a type map to + // extension data and explicitly setting the `additionalProperties` keyword to an empty object. + // Since `[ExtensionData]` can only be applied on dictionaries with `object` or `JsonElement` values + // there is no need to encode a concrete schema for thm and the catch-all empty schema is sufficient. + if (context.TypeInfo.Properties.Any(jsonPropertyInfo => jsonPropertyInfo.IsExtensionData)) + { + schema[OpenApiSchemaKeywords.AdditionalPropertiesKeyword] = new JsonObject(); + } var createSchemaReferenceId = optionsMonitor.Get(documentName).CreateSchemaReferenceId; schema.ApplyPrimitiveTypesAndFormats(context, createSchemaReferenceId); schema.ApplySchemaReferenceId(context, createSchemaReferenceId); diff --git a/src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt b/src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt index 13d82fc07859..9b411f4862b8 100644 --- a/src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt +++ b/src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt @@ -359,6 +359,25 @@ } } } + }, + "/schemas-by-ref/problem-details": { + "post": { + "tags": [ + "Sample" + ], + "responses": { + "500": { + "description": "Internal Server Error", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } } }, "components": { @@ -440,6 +459,33 @@ } } }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": { } + }, "Product": { "type": "object", "properties": { diff --git a/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.AdditionalProperties.cs b/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.AdditionalProperties.cs new file mode 100644 index 000000000000..cd975e8dc514 --- /dev/null +++ b/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.AdditionalProperties.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi.Models; + +public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase +{ + [Fact] + public async Task SetsAdditionalPropertiesOnBuiltInTypeWithExtensionData() + { + var builder = CreateBuilder(); + + builder.MapPost("/", () => new ProblemDetails()); + + await VerifyOpenApiDocument(builder, document => + { + var schema = document.Components.Schemas["ProblemDetails"]; + Assert.NotNull(schema.AdditionalProperties); + Assert.Null(schema.AdditionalProperties.Type); + }); + } + + [Fact] + public async Task SetAdditionalPropertiesOnDefinedTypeWithExtensionData() + { + var builder = CreateBuilder(); + + builder.MapPost("/", () => new MyExtensionDataType("test")); + + await VerifyOpenApiDocument(builder, document => + { + var schema = document.Components.Schemas["MyExtensionDataType"]; + Assert.NotNull(schema.AdditionalProperties); + Assert.Null(schema.AdditionalProperties.Type); + }); + } + + [Fact] + public async Task SetsAdditionalPropertiesOnDictionaryTypesWithSchema() + { + var builder = CreateBuilder(); + + builder.MapPost("/", (Dictionary data) => { }); + + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[OperationType.Post]; + var schema = operation.RequestBody.Content["application/json"].Schema.GetEffective(document); + Assert.NotNull(schema.AdditionalProperties); + Assert.Equal("string", schema.AdditionalProperties.Type); + Assert.Equal("uuid", schema.AdditionalProperties.Format); + }); + } + + private class MyExtensionDataType(string name) + { + public string Name => name; + + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } + } +}