Skip to content

Commit 32fb216

Browse files
committed
fix: add $schema sibling preservation and fix $defs location tracking
Add $schema dialect URI as a sibling override on JsonSchemaReference, matching the pattern used for the other JSON Schema 2020-12 keywords. Also fix the $defs parsing loop in V31/V32 LoadSchema to push/pop the parsing context location stack (context.StartObject/EndObject) around each LoadSchema call, mirroring JsonNodeHelper.CreateMap. Without this, nested schemas inside a reference's $defs get incorrect nodeLocation values, breaking relative $ref resolution and source-pointer diagnostics. Adds a scalar round-trip test covering $id, $schema, $comment, $anchor, $dynamicRef serialization. Ref: #2895
1 parent ef3a1a6 commit 32fb216

6 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
6464
/// </summary>
6565
public string? SchemaId { get; set; }
6666

67+
/// <summary>
68+
/// The $schema dialect URI which by default SHOULD override that of the referenced component.
69+
/// </summary>
70+
public Uri? Schema { get; set; }
71+
6772
/// <summary>
6873
/// A $comment which by default SHOULD override that of the referenced component.
6974
/// </summary>
@@ -113,6 +118,7 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
113118
Examples = reference.Examples;
114119
Extensions = reference.Extensions != null ? new Dictionary<string, IOpenApiExtension>(reference.Extensions) : null;
115120
SchemaId = reference.SchemaId;
121+
Schema = reference.Schema;
116122
Comment = reference.Comment;
117123
Vocabulary = reference.Vocabulary != null ? new Dictionary<string, bool>(reference.Vocabulary) : null;
118124
DynamicRef = reference.DynamicRef;
@@ -140,6 +146,7 @@ private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, OpenApiSpec
140146

141147
// JSON Schema 2020-12 keyword siblings (preserved per OAS 3.1+ / JSON Schema 2020-12 semantics)
142148
writer.WriteProperty(OpenApiConstants.Id, SchemaId);
149+
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
143150
writer.WriteProperty(OpenApiConstants.Comment, Comment);
144151
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
145152
if (version == OpenApiSpecVersion.OpenApi3_1)
@@ -233,6 +240,12 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject
233240
SchemaId = id;
234241
}
235242

243+
var schemaValue = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DollarSchema);
244+
if (!string.IsNullOrEmpty(schemaValue) && Uri.TryCreate(schemaValue, UriKind.Absolute, out var schemaUri))
245+
{
246+
Schema = schemaUri;
247+
}
248+
236249
var comment = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Comment);
237250
if (!string.IsNullOrEmpty(comment))
238251
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public string? Title
4949
set => Reference.Title = value;
5050
}
5151
/// <inheritdoc/>
52-
public Uri? Schema { get => Target?.Schema; }
52+
public Uri? Schema { get => Reference.Schema ?? Target?.Schema; }
5353
/// <inheritdoc/>
5454
public string? Id { get => string.IsNullOrEmpty(Reference.SchemaId) ? Target?.Id : Reference.SchemaId; }
5555
/// <inheritdoc/>

src/Microsoft.OpenApi/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Microsoft.OpenApi.JsonSchemaReference.DynamicAnchor.get -> string?
99
Microsoft.OpenApi.JsonSchemaReference.DynamicAnchor.set -> void
1010
Microsoft.OpenApi.JsonSchemaReference.DynamicRef.get -> string?
1111
Microsoft.OpenApi.JsonSchemaReference.DynamicRef.set -> void
12+
Microsoft.OpenApi.JsonSchemaReference.Schema.get -> System.Uri?
13+
Microsoft.OpenApi.JsonSchemaReference.Schema.set -> void
1214
Microsoft.OpenApi.JsonSchemaReference.SchemaId.get -> string?
1315
Microsoft.OpenApi.JsonSchemaReference.SchemaId.set -> void
1416
Microsoft.OpenApi.JsonSchemaReference.Vocabulary.get -> System.Collections.Generic.IDictionary<string!, bool>?

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,16 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
460460
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
461461
foreach (var kvp in defsObj)
462462
{
463-
if (kvp.Value is not null)
463+
if (kvp.Value is null) continue;
464+
context.StartObject(kvp.Key);
465+
try
464466
{
465467
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
466468
}
469+
finally
470+
{
471+
context.EndObject();
472+
}
467473
}
468474
result.Reference.Definitions = defs;
469475
}

src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,16 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
460460
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
461461
foreach (var kvp in defsObj)
462462
{
463-
if (kvp.Value is not null)
463+
if (kvp.Value is null) continue;
464+
context.StartObject(kvp.Key);
465+
try
464466
{
465467
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
466468
}
469+
finally
470+
{
471+
context.EndObject();
472+
}
467473
}
468474
result.Reference.Definitions = defs;
469475
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,7 @@ public async Task ParseSchemaReferencePreservesScalarKeywordSiblings()
10391039
Referencing:
10401040
$ref: '#/components/schemas/Target'
10411041
$id: 'https://example.com/referencing.json'
1042+
$schema: 'https://json-schema.org/draft/2020-12/schema'
10421043
$comment: A comment sibling
10431044
$anchor: myAnchor
10441045
$dynamicRef: '#myAnchor'
@@ -1052,6 +1053,50 @@ public async Task ParseSchemaReferencePreservesScalarKeywordSiblings()
10521053
// Assert
10531054
referencing.Should().BeOfType<OpenApiSchemaReference>();
10541055
referencing.Id.Should().Be("https://example.com/referencing.json");
1056+
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
1057+
referencing.Comment.Should().Be("A comment sibling");
1058+
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
1059+
referencing.DynamicRef.Should().Be("#myAnchor");
1060+
}
1061+
1062+
[Fact]
1063+
public async Task SerializeSchemaReferencePreservesScalarKeywordSiblings()
1064+
{
1065+
// Arrange
1066+
var yaml = """
1067+
openapi: 3.1.0
1068+
info:
1069+
title: Scalar round-trip
1070+
version: 1.0.0
1071+
paths: {}
1072+
components:
1073+
schemas:
1074+
Target:
1075+
type: object
1076+
Referencing:
1077+
$ref: '#/components/schemas/Target'
1078+
$id: 'https://example.com/referencing.json'
1079+
$schema: 'https://json-schema.org/draft/2020-12/schema'
1080+
$comment: A comment sibling
1081+
$anchor: myAnchor
1082+
$dynamicRef: '#myAnchor'
1083+
""";
1084+
1085+
// Act — parse then serialize back
1086+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
1087+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
1088+
var writer = new StringWriter();
1089+
result.Document.SerializeAsV31(new OpenApiYamlWriter(writer));
1090+
var output = writer.ToString();
1091+
1092+
// Assert — round-trip preserves scalar siblings alongside $ref
1093+
using var roundTripStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(output));
1094+
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
1095+
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
1096+
1097+
referencing.Should().BeOfType<OpenApiSchemaReference>();
1098+
referencing.Id.Should().Be("https://example.com/referencing.json");
1099+
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
10551100
referencing.Comment.Should().Be("A comment sibling");
10561101
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
10571102
referencing.DynamicRef.Should().Be("#myAnchor");

0 commit comments

Comments
 (0)