Skip to content

Commit 0c19713

Browse files
committed
Set additionalProperties when [JsonExtensionData] is present
1 parent cd24d14 commit 0c19713

File tree

4 files changed

+123
-1
lines changed

4 files changed

+123
-1
lines changed

src/OpenApi/sample/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Frozen;
54
using System.Collections.Immutable;
65
using System.ComponentModel;
76
using Microsoft.AspNetCore.Http.HttpResults;
@@ -111,6 +110,7 @@
111110
schemas.MapPost("/shape", (Shape shape) => { });
112111
schemas.MapPost("/weatherforecastbase", (WeatherForecastBase forecast) => { });
113112
schemas.MapPost("/person", (Person person) => { });
113+
schemas.MapPost("/problem-details", () => { }).ProducesProblem(statusCode: 500);
114114

115115
app.MapControllers();
116116

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ internal sealed class OpenApiSchemaService(
8888
{
8989
schema = new JsonObject();
9090
}
91+
// STJ operates under the assumption that JSON Schema makes around `additionalProperties` being
92+
// true by default for all schemas. OpenAPI v3 takes a different interpretation and assumes that
93+
// `additionalProperties` is false by default. We override the default behavior assumed by STJ to
94+
// match the OpenAPI v3 interpretation here by checking to see if any properties on a type map to
95+
// extension data and explicitly setting the `additionalProperties` keyword to an empty object.
96+
// Since `[ExtensionData]` can only be applied on dictionaries with `object` or `JsonElement` values
97+
// there is no need to encode a concrete schema for thm and the catch-all empty schema is sufficient.
98+
if (context.TypeInfo.Properties.Any(jsonPropertyInfo => jsonPropertyInfo.IsExtensionData))
99+
{
100+
schema[OpenApiSchemaKeywords.AdditionalPropertiesKeyword] = new JsonObject();
101+
}
91102
schema.ApplyPrimitiveTypesAndFormats(context);
92103
schema.ApplySchemaReferenceId(context);
93104
schema.ApplyPolymorphismOptions(context);

src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,25 @@
359359
}
360360
}
361361
}
362+
},
363+
"/schemas-by-ref/problem-details": {
364+
"post": {
365+
"tags": [
366+
"Sample"
367+
],
368+
"responses": {
369+
"500": {
370+
"description": "Internal Server Error",
371+
"content": {
372+
"application/problem+json": {
373+
"schema": {
374+
"$ref": "#/components/schemas/ProblemDetails"
375+
}
376+
}
377+
}
378+
}
379+
}
380+
}
362381
}
363382
},
364383
"components": {
@@ -440,6 +459,33 @@
440459
}
441460
}
442461
},
462+
"ProblemDetails": {
463+
"type": "object",
464+
"properties": {
465+
"type": {
466+
"type": "string",
467+
"nullable": true
468+
},
469+
"title": {
470+
"type": "string",
471+
"nullable": true
472+
},
473+
"status": {
474+
"type": "integer",
475+
"format": "int32",
476+
"nullable": true
477+
},
478+
"detail": {
479+
"type": "string",
480+
"nullable": true
481+
},
482+
"instance": {
483+
"type": "string",
484+
"nullable": true
485+
}
486+
},
487+
"additionalProperties": { }
488+
},
443489
"Product": {
444490
"type": "object",
445491
"properties": {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json.Serialization;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.OpenApi.Models;
8+
9+
public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase
10+
{
11+
[Fact]
12+
public async Task SetsAdditionalPropertiesOnBuiltInTypeWithExtensionData()
13+
{
14+
var builder = CreateBuilder();
15+
16+
builder.MapPost("/", () => new ProblemDetails());
17+
18+
await VerifyOpenApiDocument(builder, document =>
19+
{
20+
var schema = document.Components.Schemas["ProblemDetails"];
21+
Assert.NotNull(schema.AdditionalProperties);
22+
Assert.Null(schema.AdditionalProperties.Type);
23+
});
24+
}
25+
26+
[Fact]
27+
public async Task SetAdditionalPropertiesOnDefinedTypeWithExtensionData()
28+
{
29+
var builder = CreateBuilder();
30+
31+
builder.MapPost("/", () => new MyExtensionDataType("test"));
32+
33+
await VerifyOpenApiDocument(builder, document =>
34+
{
35+
var schema = document.Components.Schemas["MyExtensionDataType"];
36+
Assert.NotNull(schema.AdditionalProperties);
37+
Assert.Null(schema.AdditionalProperties.Type);
38+
});
39+
}
40+
41+
[Fact]
42+
public async Task SetsAdditionalPropertiesOnDictionaryTypesWithSchema()
43+
{
44+
var builder = CreateBuilder();
45+
46+
builder.MapPost("/", (Dictionary<string, Guid> data) => { });
47+
48+
await VerifyOpenApiDocument(builder, document =>
49+
{
50+
var operation = document.Paths["/"].Operations[OperationType.Post];
51+
var schema = operation.RequestBody.Content["application/json"].Schema.GetEffective(document);
52+
Assert.NotNull(schema.AdditionalProperties);
53+
Assert.Equal("string", schema.AdditionalProperties.Type);
54+
Assert.Equal("uuid", schema.AdditionalProperties.Format);
55+
});
56+
}
57+
58+
private class MyExtensionDataType(string name)
59+
{
60+
public string Name => name;
61+
62+
[JsonExtensionData]
63+
public IDictionary<string, object> ExtensionData { get; set; }
64+
}
65+
}

0 commit comments

Comments
 (0)