Skip to content

Commit a2d21f9

Browse files
authored
Merge pull request #2792 from microsoft/fix/ref-peer-keywords-to-v2
Merge pull request #2782 from microsoft/copilot/fix-openapi-schema-reference-description
2 parents 5d58a8d + c261ed7 commit a2d21f9

8 files changed

+193
-8
lines changed

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System;
@@ -52,6 +52,12 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
5252
/// </summary>
5353
public IList<JsonNode>? Examples { get; set; }
5454

55+
/// <summary>
56+
/// Extension data for this schema reference. Only allowed in OpenAPI 3.1 and later.
57+
/// Extensions are NOT written when serializing for OpenAPI 2.0 or 3.0.
58+
/// </summary>
59+
public IDictionary<string, IOpenApiExtension>? Extensions { get; set; }
60+
5561
/// <summary>
5662
/// Parameterless constructor
5763
/// </summary>
@@ -69,6 +75,7 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
6975
ReadOnly = reference.ReadOnly;
7076
WriteOnly = reference.WriteOnly;
7177
Examples = reference.Examples;
78+
Extensions = reference.Extensions != null ? new Dictionary<string, IOpenApiExtension>(reference.Extensions) : null;
7279
}
7380

7481
/// <inheritdoc/>
@@ -97,6 +104,7 @@ protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
97104
{
98105
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (w, e) => w.WriteAny(e));
99106
}
107+
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_1);
100108
}
101109

102110
/// <inheritdoc/>
@@ -137,5 +145,15 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject
137145
{
138146
Examples = examplesArray.OfType<JsonNode>().ToList();
139147
}
148+
149+
// Extensions (properties starting with "x-")
150+
foreach (var property in jsonObject
151+
.Where(static p => p.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase)
152+
&& p.Value is not null))
153+
{
154+
var extensionValue = property.Value!;
155+
Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase);
156+
Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone());
157+
}
140158
}
141159
}

src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.OpenApi
1010
/// <summary>
1111
/// Schema reference object
1212
/// </summary>
13-
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties
13+
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiExtensible
1414
{
1515

1616
/// <summary>
@@ -158,7 +158,11 @@ public bool Deprecated
158158
/// <inheritdoc/>
159159
public OpenApiXml? Xml { get => Target?.Xml; }
160160
/// <inheritdoc/>
161-
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
161+
public IDictionary<string, IOpenApiExtension>? Extensions
162+
{
163+
get => Reference.Extensions ?? Target?.Extensions;
164+
set => Reference.Extensions = value;
165+
}
162166

163167
/// <inheritdoc/>
164168
public IDictionary<string, JsonNode>? UnrecognizedKeywords { get => Target?.UnrecognizedKeywords; }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary<string!, Microsoft.OpenApi.IOpenApiExtension!>?
3+
Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void
4+
Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"$ref": "#/definitions/Pet"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"$ref":"#/definitions/Pet"}

test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=False.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
"examples": [
88
"reference example"
99
],
10+
"x-custom": "custom value",
1011
"$ref": "#/components/schemas/Pet"
1112
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"$ref":"#/components/schemas/Pet"}
1+
{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"x-custom":"custom value","$ref":"#/components/schemas/Pet"}

test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System.Collections.Generic;
@@ -133,7 +133,11 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput
133133
WriteOnly = false,
134134
Deprecated = true,
135135
Default = JsonValue.Create("reference default"),
136-
Examples = new List<JsonNode> { JsonValue.Create("reference example") }
136+
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
137+
Extensions = new Dictionary<string, IOpenApiExtension>
138+
{
139+
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
140+
}
137141
};
138142

139143
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
@@ -152,7 +156,7 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput
152156
[InlineData(false)]
153157
public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput)
154158
{
155-
// Arrange
159+
// Arrange - Extensions should NOT appear in v3.0 output
156160
var reference = new OpenApiSchemaReference("Pet", null)
157161
{
158162
Title = "Reference Title",
@@ -161,7 +165,11 @@ public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput)
161165
WriteOnly = false,
162166
Deprecated = true,
163167
Default = JsonValue.Create("reference default"),
164-
Examples = new List<JsonNode> { JsonValue.Create("reference example") }
168+
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
169+
Extensions = new Dictionary<string, IOpenApiExtension>
170+
{
171+
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
172+
}
165173
};
166174

167175
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
@@ -175,6 +183,38 @@ public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput)
175183
await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput);
176184
}
177185

186+
[Theory]
187+
[InlineData(true)]
188+
[InlineData(false)]
189+
public async Task SerializeSchemaReferenceAsV2JsonWorks(bool produceTerseOutput)
190+
{
191+
// Arrange - Extensions should NOT appear in v2 output
192+
var reference = new OpenApiSchemaReference("Pet", null)
193+
{
194+
Title = "Reference Title",
195+
Description = "Reference Description",
196+
ReadOnly = true,
197+
WriteOnly = false,
198+
Deprecated = true,
199+
Default = JsonValue.Create("reference default"),
200+
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
201+
Extensions = new Dictionary<string, IOpenApiExtension>
202+
{
203+
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
204+
}
205+
};
206+
207+
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
208+
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput });
209+
210+
// Act
211+
reference.SerializeAsV2(writer);
212+
await writer.FlushAsync();
213+
214+
// Assert
215+
await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput);
216+
}
217+
178218
[Fact]
179219
public void ParseSchemaReferenceWithAnnotationsWorks()
180220
{
@@ -256,5 +296,120 @@ public void ParseSchemaReferenceWithAnnotationsWorks()
256296
Assert.Equal("Original Pet Title", targetSchema.Title);
257297
Assert.Equal("Original Pet Description", targetSchema.Description);
258298
}
299+
300+
[Fact]
301+
public void ParseSchemaReferenceWithExtensionsWorks()
302+
{
303+
// Arrange
304+
var jsonContent = @"{
305+
""openapi"": ""3.1.0"",
306+
""info"": {
307+
""title"": ""Test API"",
308+
""version"": ""1.0.0""
309+
},
310+
""paths"": {
311+
""/test"": {
312+
""get"": {
313+
""responses"": {
314+
""200"": {
315+
""description"": ""OK"",
316+
""content"": {
317+
""application/json"": {
318+
""schema"": {
319+
""$ref"": ""#/components/schemas/Pet"",
320+
""description"": ""A pet object"",
321+
""x-custom-extension"": ""custom value"",
322+
""x-another-extension"": 42
323+
}
324+
}
325+
}
326+
}
327+
}
328+
}
329+
}
330+
},
331+
""components"": {
332+
""schemas"": {
333+
""Pet"": {
334+
""type"": ""object"",
335+
""properties"": {
336+
""name"": {
337+
""type"": ""string""
338+
}
339+
}
340+
}
341+
}
342+
}
343+
}";
344+
345+
// Act
346+
var readResult = OpenApiDocument.Parse(jsonContent, "json");
347+
var document = readResult.Document;
348+
349+
// Assert
350+
Assert.NotNull(document);
351+
Assert.Empty(readResult.Diagnostic.Errors);
352+
353+
var schema = document.Paths["/test"].Operations[HttpMethod.Get]
354+
.Responses["200"].Content["application/json"].Schema;
355+
356+
Assert.IsType<OpenApiSchemaReference>(schema);
357+
var schemaRef = (OpenApiSchemaReference)schema;
358+
359+
// Test that reference-level extensions are parsed
360+
Assert.NotNull(schemaRef.Extensions);
361+
Assert.Contains("x-custom-extension", schemaRef.Extensions.Keys);
362+
Assert.Contains("x-another-extension", schemaRef.Extensions.Keys);
363+
}
364+
365+
[Fact]
366+
public async Task SchemaReferenceExtensionsNotWrittenInV30()
367+
{
368+
// Arrange
369+
var reference = new OpenApiSchemaReference("Pet", null)
370+
{
371+
Description = "Local description",
372+
Extensions = new Dictionary<string, IOpenApiExtension>
373+
{
374+
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
375+
}
376+
};
377+
378+
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
379+
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true });
380+
381+
// Act
382+
reference.SerializeAsV3(writer);
383+
await writer.FlushAsync();
384+
var output = outputStringWriter.ToString();
385+
386+
// Assert: In v3.0, ONLY $ref should appear - no description, no extensions
387+
Assert.Equal(@"{""$ref"":""#/components/schemas/Pet""}", output);
388+
}
389+
390+
[Fact]
391+
public async Task SchemaReferenceExtensionsNotWrittenInV2()
392+
{
393+
// Arrange
394+
var reference = new OpenApiSchemaReference("Pet", null)
395+
{
396+
Description = "Local description",
397+
Extensions = new Dictionary<string, IOpenApiExtension>
398+
{
399+
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
400+
}
401+
};
402+
403+
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
404+
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true });
405+
406+
// Act
407+
reference.SerializeAsV2(writer);
408+
await writer.FlushAsync();
409+
var output = outputStringWriter.ToString();
410+
411+
// Assert: In v2, ONLY $ref should appear - no description, no extensions
412+
Assert.Equal(@"{""$ref"":""#/definitions/Pet""}", output);
413+
}
259414
}
260415
}

0 commit comments

Comments
 (0)