Skip to content

Commit 005db34

Browse files
committed
fix(schema): remove Count==1 guard so works with siblings
Allows schemas with annotation siblings (description, title, , etc.) to still create an OpenApiSchemaReference with Target resolution, matching how already handles siblings. Adds parsing to the branch, matching the branch.
1 parent 940c4d0 commit 005db34

3 files changed

Lines changed: 91 additions & 10 deletions

File tree

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,17 +468,22 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
468468
return result;
469469
}
470470

471-
if (dynamicPointer != null && jsonObject.Count == 1)
471+
if (dynamicPointer != null)
472472
{
473-
// A pure, bare $dynamicRef (no other schema keywords): create an OpenApiSchemaReference
474-
// whose Target resolves via the $dynamicAnchor index instead of the $ref URI lookup.
475-
// When siblings are present (type, properties, maxProperties, $defs, etc.) fall through to
476-
// ParseMap so they are preserved on a normal OpenApiSchema instead of being dropped.
477473
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
478474
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
479475
result.Reference.DynamicRef = dynamicPointer;
480476
result.Reference.IsDynamicRefOnly = true;
481477
result.Reference.SetMetadataFromJsonObject(jsonObject);
478+
479+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var dynDefsNode) && dynDefsNode is JsonObject)
480+
{
481+
context.StartObject(OpenApiConstants.Defs);
482+
if (dynDefsNode.CreateMap(LoadSchema, hostDocument, context) is { Count: > 0 } dynDefinitions)
483+
result.Reference.Definitions = dynDefinitions;
484+
context.EndObject();
485+
}
486+
482487
return result;
483488
}
484489

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,17 +468,22 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
468468
return result;
469469
}
470470

471-
if (dynamicPointer != null && jsonObject.Count == 1)
471+
if (dynamicPointer != null)
472472
{
473-
// A pure, bare $dynamicRef (no other schema keywords): create an OpenApiSchemaReference
474-
// whose Target resolves via the $dynamicAnchor index instead of the $ref URI lookup.
475-
// When siblings are present (type, properties, maxProperties, $defs, etc.) fall through to
476-
// ParseMap so they are preserved on a normal OpenApiSchema instead of being dropped.
477473
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
478474
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
479475
result.Reference.DynamicRef = dynamicPointer;
480476
result.Reference.IsDynamicRefOnly = true;
481477
result.Reference.SetMetadataFromJsonObject(jsonObject);
478+
479+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var dynDefsNode) && dynDefsNode is JsonObject)
480+
{
481+
context.StartObject(OpenApiConstants.Defs);
482+
if (dynDefsNode.CreateMap(LoadSchema, hostDocument, context) is { Count: > 0 } dynDefinitions)
483+
result.Reference.Definitions = dynDefinitions;
484+
context.EndObject();
485+
}
486+
482487
return result;
483488
}
484489

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,4 +706,75 @@ public async Task CreateShallowCopyPreservesValidationSiblings()
706706
Assert.NotNull(copy.AllOf);
707707
Assert.Single(copy.AllOf);
708708
}
709+
710+
[Fact]
711+
public async Task DynamicRefWithAnnotationSiblingResolvesAndPreservesSibling()
712+
{
713+
var yaml =
714+
"""
715+
openapi: 3.1.0
716+
info:
717+
title: Test
718+
version: 1.0.0
719+
paths: {}
720+
components:
721+
schemas:
722+
Tree:
723+
$dynamicAnchor: node
724+
type: object
725+
properties:
726+
value:
727+
type: string
728+
next:
729+
$dynamicRef: '#node'
730+
description: The next node in the tree
731+
""";
732+
733+
var doc = await LoadDocumentAsync(yaml);
734+
735+
var tree = doc.Components.Schemas["Tree"];
736+
var nextSchema = tree.Properties["next"];
737+
738+
var reference = Assert.IsType<OpenApiSchemaReference>(nextSchema);
739+
Assert.True(reference.Reference.IsDynamicRefOnly);
740+
Assert.NotNull(reference.Target);
741+
Assert.Same(tree, reference.Target);
742+
Assert.Equal("The next node in the tree", reference.Description);
743+
}
744+
745+
[Fact]
746+
public async Task DynamicRefWithDefsSiblingParsesDefs()
747+
{
748+
var yaml =
749+
"""
750+
openapi: 3.1.0
751+
info:
752+
title: Test
753+
version: 1.0.0
754+
paths: {}
755+
components:
756+
schemas:
757+
Tree:
758+
$dynamicAnchor: node
759+
type: object
760+
properties:
761+
next:
762+
$dynamicRef: '#node'
763+
$defs:
764+
extra:
765+
type: string
766+
""";
767+
768+
var doc = await LoadDocumentAsync(yaml);
769+
770+
var tree = doc.Components.Schemas["Tree"];
771+
var nextSchema = tree.Properties["next"];
772+
773+
var reference = Assert.IsType<OpenApiSchemaReference>(nextSchema);
774+
Assert.True(reference.Reference.IsDynamicRefOnly);
775+
Assert.NotNull(reference.Target);
776+
Assert.Same(tree, reference.Target);
777+
Assert.NotNull(reference.Definitions);
778+
Assert.True(reference.Definitions.ContainsKey("extra"));
779+
}
709780
}

0 commit comments

Comments
 (0)