Skip to content

Commit ef3a1a6

Browse files
committed
fix: align scalar extraction pattern with existing Title convention
Switch from GetPropertyValueFromNode(...) ?? X to the if (!string.IsNullOrEmpty(...)) pattern used by the existing Title/annotation extraction, for reviewer consistency. Also add test for the allOf-based binding variant where $defs sits inside allOf[0] and the nested schema has $ref + $dynamicAnchor (the pattern from the blocker analysis). Ref: #2895
1 parent 7bcdecb commit ef3a1a6

2 files changed

Lines changed: 82 additions & 5 deletions

File tree

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,35 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject
227227

228228
// JSON Schema 2020-12 keyword siblings ($defs is parsed separately in the deserializer
229229
// because it requires LoadSchema for nested schema materialization)
230-
SchemaId = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Id) ?? SchemaId;
231-
Comment = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Comment) ?? Comment;
232-
DynamicRef = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DynamicRef) ?? DynamicRef;
233-
DynamicAnchor = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DynamicAnchor) ?? DynamicAnchor;
234-
Anchor = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Anchor) ?? Anchor;
230+
var id = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Id);
231+
if (!string.IsNullOrEmpty(id))
232+
{
233+
SchemaId = id;
234+
}
235+
236+
var comment = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Comment);
237+
if (!string.IsNullOrEmpty(comment))
238+
{
239+
Comment = comment;
240+
}
241+
242+
var dynamicRef = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DynamicRef);
243+
if (!string.IsNullOrEmpty(dynamicRef))
244+
{
245+
DynamicRef = dynamicRef;
246+
}
247+
248+
var dynamicAnchor = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DynamicAnchor);
249+
if (!string.IsNullOrEmpty(dynamicAnchor))
250+
{
251+
DynamicAnchor = dynamicAnchor;
252+
}
253+
254+
var anchor = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Anchor);
255+
if (!string.IsNullOrEmpty(anchor))
256+
{
257+
Anchor = anchor;
258+
}
235259

236260
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Vocabulary, out var vocabNode) && vocabNode is JsonObject vocabObj)
237261
{

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,5 +1090,58 @@ public async Task ParseSchemaReferencePreservesVocabularySibling()
10901090
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"].Should().BeTrue();
10911091
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"].Should().BeFalse();
10921092
}
1093+
1094+
[Fact]
1095+
public async Task ParseSchemaReferencePreservesDynamicAnchorInsideDefsInAllOf()
1096+
{
1097+
// Arrange — the allOf-based binding variant: $defs sits inside allOf[0],
1098+
// and the nested schema has $ref + $dynamicAnchor (the binding entry).
1099+
// This was called out as a real-world pattern that hits the same root cause
1100+
// because the inner schema is an OpenApiSchemaReference whose sibling was dropped.
1101+
var yaml = """
1102+
openapi: 3.1.0
1103+
info:
1104+
title: allOf binding variant
1105+
version: 1.0.0
1106+
paths: {}
1107+
components:
1108+
schemas:
1109+
Asset:
1110+
type: object
1111+
properties:
1112+
id:
1113+
type: string
1114+
Paged:
1115+
type: object
1116+
properties:
1117+
items:
1118+
type: array
1119+
AssetPaged:
1120+
allOf:
1121+
- $defs:
1122+
contentType:
1123+
$dynamicAnchor: contentType
1124+
$ref: '#/components/schemas/Asset'
1125+
- $ref: '#/components/schemas/Paged'
1126+
""";
1127+
1128+
// Act
1129+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
1130+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
1131+
var assetPaged = result.Document.Components!.Schemas["AssetPaged"];
1132+
1133+
// Assert — the binding entry inside $defs/allOf[0] is reachable
1134+
// allOf[0] is a regular OpenApiSchema (no $ref at top level), so $defs is parsed normally.
1135+
// The nested contentType schema is an OpenApiSchemaReference ($ref: Asset),
1136+
// and its $dynamicAnchor sibling must be preserved.
1137+
assetPaged.AllOf.Should().NotBeNull();
1138+
assetPaged.AllOf!.Count.Should().Be(2);
1139+
var defsHolder = assetPaged.AllOf[0];
1140+
defsHolder.Definitions.Should().NotBeNull();
1141+
defsHolder.Definitions!.Should().ContainKey("contentType");
1142+
var contentType = defsHolder.Definitions["contentType"];
1143+
contentType.Should().BeOfType<OpenApiSchemaReference>();
1144+
contentType.DynamicAnchor.Should().Be("contentType");
1145+
}
10931146
}
10941147
}

0 commit comments

Comments
 (0)