Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 19 additions & 1 deletion src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
Expand Down Expand Up @@ -52,6 +52,12 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
/// </summary>
public IList<JsonNode>? Examples { get; set; }

/// <summary>
/// Extension data for this schema reference. Only allowed in OpenAPI 3.1 and later.
/// Extensions are NOT written when serializing for OpenAPI 2.0 or 3.0.
/// </summary>
public IDictionary<string, IOpenApiExtension>? Extensions { get; set; }

/// <summary>
/// Parameterless constructor
/// </summary>
Expand All @@ -69,6 +75,7 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
ReadOnly = reference.ReadOnly;
WriteOnly = reference.WriteOnly;
Examples = reference.Examples;
Extensions = reference.Extensions != null ? new Dictionary<string, IOpenApiExtension>(reference.Extensions) : null;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -97,6 +104,7 @@ protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
{
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (w, e) => w.WriteAny(e));
}
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_1);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -137,5 +145,15 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject
{
Examples = examplesArray.OfType<JsonNode>().ToList();
}

// Extensions (properties starting with "x-")
foreach (var property in jsonObject
.Where(static p => p.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase)
&& p.Value is not null))
{
var extensionValue = property.Value!;
Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase);
Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.OpenApi
/// <summary>
/// Schema reference object
/// </summary>
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiExtensible
{

/// <summary>
Expand Down Expand Up @@ -158,7 +158,11 @@ public bool Deprecated
/// <inheritdoc/>
public OpenApiXml? Xml { get => Target?.Xml; }
/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions
{
get => Reference.Extensions ?? Target?.Extensions;
set => Reference.Extensions = value;
}

/// <inheritdoc/>
public IDictionary<string, JsonNode>? UnrecognizedKeywords { get => Target?.UnrecognizedKeywords; }
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
#nullable enable
Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary<string!, Microsoft.OpenApi.IOpenApiExtension!>?
Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void
Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$ref": "#/definitions/Pet"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$ref":"#/definitions/Pet"}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"examples": [
"reference example"
],
"x-custom": "custom value",
"$ref": "#/components/schemas/Pet"
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"$ref":"#/components/schemas/Pet"}
{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"x-custom":"custom value","$ref":"#/components/schemas/Pet"}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
Expand Down Expand Up @@ -133,7 +133,11 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput
WriteOnly = false,
Deprecated = true,
Default = JsonValue.Create("reference default"),
Examples = new List<JsonNode> { JsonValue.Create("reference example") }
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
}
};

var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
Expand All @@ -152,7 +156,7 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput
[InlineData(false)]
public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput)
{
// Arrange
// Arrange - Extensions should NOT appear in v3.0 output
var reference = new OpenApiSchemaReference("Pet", null)
{
Title = "Reference Title",
Expand All @@ -161,7 +165,11 @@ public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput)
WriteOnly = false,
Deprecated = true,
Default = JsonValue.Create("reference default"),
Examples = new List<JsonNode> { JsonValue.Create("reference example") }
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
}
};

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

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task SerializeSchemaReferenceAsV2JsonWorks(bool produceTerseOutput)
{
// Arrange - Extensions should NOT appear in v2 output
var reference = new OpenApiSchemaReference("Pet", null)
{
Title = "Reference Title",
Description = "Reference Description",
ReadOnly = true,
WriteOnly = false,
Deprecated = true,
Default = JsonValue.Create("reference default"),
Examples = new List<JsonNode> { JsonValue.Create("reference example") },
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
}
};

var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput });

// Act
reference.SerializeAsV2(writer);
await writer.FlushAsync();

// Assert
await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput);
}

[Fact]
public void ParseSchemaReferenceWithAnnotationsWorks()
{
Expand Down Expand Up @@ -256,5 +296,120 @@ public void ParseSchemaReferenceWithAnnotationsWorks()
Assert.Equal("Original Pet Title", targetSchema.Title);
Assert.Equal("Original Pet Description", targetSchema.Description);
}

[Fact]
public void ParseSchemaReferenceWithExtensionsWorks()
{
// Arrange
var jsonContent = @"{
""openapi"": ""3.1.0"",
""info"": {
""title"": ""Test API"",
""version"": ""1.0.0""
},
""paths"": {
""/test"": {
""get"": {
""responses"": {
""200"": {
""description"": ""OK"",
""content"": {
""application/json"": {
""schema"": {
""$ref"": ""#/components/schemas/Pet"",
""description"": ""A pet object"",
""x-custom-extension"": ""custom value"",
""x-another-extension"": 42
}
}
}
}
}
}
}
},
""components"": {
""schemas"": {
""Pet"": {
""type"": ""object"",
""properties"": {
""name"": {
""type"": ""string""
}
}
}
}
}
}";

// Act
var readResult = OpenApiDocument.Parse(jsonContent, "json");
var document = readResult.Document;

// Assert
Assert.NotNull(document);
Assert.Empty(readResult.Diagnostic.Errors);

var schema = document.Paths["/test"].Operations[HttpMethod.Get]
.Responses["200"].Content["application/json"].Schema;

Assert.IsType<OpenApiSchemaReference>(schema);
var schemaRef = (OpenApiSchemaReference)schema;

// Test that reference-level extensions are parsed
Assert.NotNull(schemaRef.Extensions);
Assert.Contains("x-custom-extension", schemaRef.Extensions.Keys);
Assert.Contains("x-another-extension", schemaRef.Extensions.Keys);
}

[Fact]
public async Task SchemaReferenceExtensionsNotWrittenInV30()
{
// Arrange
var reference = new OpenApiSchemaReference("Pet", null)
{
Description = "Local description",
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
}
};

var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true });

// Act
reference.SerializeAsV3(writer);
await writer.FlushAsync();
var output = outputStringWriter.ToString();

// Assert: In v3.0, ONLY $ref should appear - no description, no extensions
Assert.Equal(@"{""$ref"":""#/components/schemas/Pet""}", output);
}

[Fact]
public async Task SchemaReferenceExtensionsNotWrittenInV2()
{
// Arrange
var reference = new OpenApiSchemaReference("Pet", null)
{
Description = "Local description",
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value"))
}
};

var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true });

// Act
reference.SerializeAsV2(writer);
await writer.FlushAsync();
var output = outputStringWriter.ToString();

// Assert: In v2, ONLY $ref should appear - no description, no extensions
Assert.Equal(@"{""$ref"":""#/definitions/Pet""}", output);
}
}
}
Loading