Describe the bug
When parsing an OpenAPI 3.1+ document, OpenApiV31Deserializer.LoadSchema short-circuits as soon as it sees a $ref: it returns an OpenApiSchemaReference and never runs ParseMap over the remaining sibling keywords. The only sibling data captured on that path is whatever Reference.SetMetadataFromJsonObject extracts, which (via JsonSchemaReference.SetAdditional31MetadataFromMapNode) is a hard-coded allow-list of annotations — title, deprecated, readOnly, writeOnly, default, examples, extensions. Every other sibling keyword — $defs, $dynamicAnchor, $dynamicRef, $id, $anchor, $vocabulary, $comment, and the rest of the JSON Schema 2020-12 vocabulary — is never parsed into the object model at all. The OpenApiSchemaReference accessors then delegate to Target, so callers observe Target's values (or null) instead of the sibling declarations present in the source document.
The keywords in question are JSON Schema 2020-12 keywords. OpenAPI 3.0 does not recognize them, and — independently — the 3.0 spec requires $ref siblings to be ignored, so dropping them on the 3.0 path is correct. The bug is specific to the 3.1+ deserializer: 3.1 / JSON Schema 2020-12 makes $ref a regular keyword whose siblings are fully valid, yet the 3.1 parser still skips them when $ref is present.
Related prior issues:
This issue is about JSON Schema 2020-12 keyword siblings being dropped when they should be preserved, on 3.1+ documents.
OpenApi File To Reproduce
openapi: 3.1.0
info:
title: Sibling preservation repro
version: 1.0.0
paths: {}
components:
schemas:
Target:
type: object
properties:
name:
type: string
Referencing:
$ref: '#/components/schemas/Target'
description: Sibling description
$dynamicAnchor: anchor
$defs:
sibling:
$dynamicAnchor: inner
$ref: '#/components/schemas/Target'
Steps to reproduce
using Microsoft.OpenApi;
using Microsoft.OpenApi.Reader;
var settings = new OpenApiReaderSettings();
settings.AddYamlReader();
var yaml = """
openapi: 3.1.0
info:
title: Sibling preservation repro
version: 1.0.0
paths: {}
components:
schemas:
Target:
type: object
properties:
name:
type: string
Referencing:
$ref: '#/components/schemas/Target'
description: Sibling description
$dynamicAnchor: anchor
$defs:
sibling:
$dynamicAnchor: inner
$ref: '#/components/schemas/Target'
""";
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
var result = await OpenApiDocument.LoadAsync(stream, "yaml", settings);
var referencing = result.Document.Components!.Schemas["Referencing"];
Console.WriteLine($"Type: {referencing.GetType().Name}");
Console.WriteLine($"Is OpenApiSchemaReference: {referencing is OpenApiSchemaReference}");
Console.WriteLine($"Description (sibling): {referencing.Description ?? "(null)"}");
Console.WriteLine($"DynamicAnchor (sibling): {referencing.DynamicAnchor ?? "(null)"}");
Console.WriteLine($"Definitions count: {referencing.Definitions?.Count ?? 0}");
if (referencing.Definitions != null)
foreach (var kvp in referencing.Definitions)
Console.WriteLine($" $defs.{kvp.Key}: anchor={kvp.Value.DynamicAnchor ?? "(null)"}");
Verified against Microsoft.OpenApi 3.7.0 and Microsoft.OpenApi.YamlReader 3.7.0.
Expected behavior
Per OpenAPI 3.1 / JSON Schema 2020-12, $ref is no longer a special keyword that overrides siblings. All sibling keywords MUST be preserved on the referencing schema and accessible through the object model:
Type: OpenApiSchemaReference
Is OpenApiSchemaReference: True
Description (sibling): Sibling description
DynamicAnchor (sibling): anchor
Definitions count: 1
$defs.sibling: anchor=inner
Actual behavior
Type: OpenApiSchemaReference
Is OpenApiSchemaReference: True
Description (sibling): Sibling description ← preserved (per #2369)
DynamicAnchor (sibling): (null) ← LOST
Definitions count: 0 ← LOST (delegates to Target.Definitions)
The sibling values aren't merely hidden by delegation — they were never parsed. OpenApiV31Deserializer.LoadSchema returns an OpenApiSchemaReference immediately on $ref (Reader/V31/OpenApiSchemaDeserializer.cs:452) and skips ParseMap entirely, so the fixed-field entries that recognize $defs / $dynamicAnchor / $id / $anchor / $vocabulary / $comment (lines 24-50) never execute. OpenApiSchemaReference's accessors then delegate to Target, which is why Target's (absent) values surface instead.
Screenshots/Code Snippets
The deeper impact is observable on the JSON Schema generics pattern (Pattern B from the $dynamicRef ecosystem). The fixture below is the canonical "reusable paginated response" pattern, validator-backed by Hyperjump:
components:
schemas:
User: { type: object, properties: { id: { type: string } } }
PaginatedTemplate:
$defs:
itemType:
$dynamicAnchor: itemType
not: {} # placeholder slot
type: object
properties:
items:
type: array
items:
$dynamicRef: '#itemType' # template slot
PaginatedUserResponse:
$ref: '#/components/schemas/PaginatedTemplate'
$defs:
itemType:
$dynamicAnchor: itemType
$ref: '#/components/schemas/User' # binding for User
After parsing:
PaginatedUserResponse is an OpenApiSchemaReference with Target = PaginatedTemplate.
PaginatedUserResponse.Definitions returns PaginatedTemplate.Definitions (the placeholder slot), not the User-bound sibling.
- There is no API path to reach the User-bound
$defs.itemType entry. The dynamic-scope binding is unreachable.
This blocks Kiota (and any other downstream tool using Microsoft.OpenApi) from emitting class PaginatedUserResponse : PaginatedTemplate<User> from this fixture.
Additional context
Spec reference
- OpenAPI 3.1: "The OpenAPI Schema Object is a superset of the JSON Schema Specification Draft 2020-12." — https://spec.openapis.org/oas/v3.1.1#schema-object
- JSON Schema 2020-12 §4.7:
$ref is a regular applicator keyword; siblings are no longer ignored. The 3.0 "When a $ref is used, all other properties SHALL be ignored" rule does not appear in 3.1.
- JSON Schema 2020-12 §7.7:
$dynamicAnchor / $dynamicRef / $defs are first-class keywords that participate in dynamic scope resolution alongside $ref.
Why this matters
The JSON Schema generics pattern (a template schema with $dynamicRef slots bound by sibling $defs in referencing schemas) is one of two primary real-world uses of $dynamicRef. It cannot be implemented on top of Microsoft.OpenApi 3.7.0 because the binding information is dropped at parse time.
A cross-generator compatibility matrix is tracked at https://github.com/aqeelat/openapi-dynamicref-adoption-tracker — multiple tools are affected by this pattern, and Microsoft.OpenApi is the upstream blocker for all .NET-based tools (Kiota today; potentially others).
Environment
Microsoft.OpenApi 3.7.0
Microsoft.OpenApi.YamlReader 3.7.0
- .NET SDK 10.0.301
- Verified on macOS arm64; behavior is platform-independent
Willingness to contribute a PR
I will open a PR soon.
This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.
Describe the bug
When parsing an OpenAPI 3.1+ document,
OpenApiV31Deserializer.LoadSchemashort-circuits as soon as it sees a$ref: it returns anOpenApiSchemaReferenceand never runsParseMapover the remaining sibling keywords. The only sibling data captured on that path is whateverReference.SetMetadataFromJsonObjectextracts, which (viaJsonSchemaReference.SetAdditional31MetadataFromMapNode) is a hard-coded allow-list of annotations —title,deprecated,readOnly,writeOnly,default,examples,extensions. Every other sibling keyword —$defs,$dynamicAnchor,$dynamicRef,$id,$anchor,$vocabulary,$comment, and the rest of the JSON Schema 2020-12 vocabulary — is never parsed into the object model at all. TheOpenApiSchemaReferenceaccessors then delegate toTarget, so callers observeTarget's values (or null) instead of the sibling declarations present in the source document.The keywords in question are JSON Schema 2020-12 keywords. OpenAPI 3.0 does not recognize them, and — independently — the 3.0 spec requires
$refsiblings to be ignored, so dropping them on the 3.0 path is correct. The bug is specific to the 3.1+ deserializer: 3.1 / JSON Schema 2020-12 makes$refa regular keyword whose siblings are fully valid, yet the 3.1 parser still skips them when$refis present.Related prior issues:
descriptionand a subset of annotation siblings but not the JSON Schema 2020-12 keyword siblings below)$refproducing non-spec-compliant output in OAS 3.0" (separate concern about annotation-sibling behavior across 3.0 vs 3.1 — explicitly out of scope for this issue)This issue is about JSON Schema 2020-12 keyword siblings being dropped when they should be preserved, on 3.1+ documents.
OpenApi File To Reproduce
Steps to reproduce
Verified against
Microsoft.OpenApi3.7.0andMicrosoft.OpenApi.YamlReader3.7.0.Expected behavior
Per OpenAPI 3.1 / JSON Schema 2020-12,
$refis no longer a special keyword that overrides siblings. All sibling keywords MUST be preserved on the referencing schema and accessible through the object model:Actual behavior
The sibling values aren't merely hidden by delegation — they were never parsed.
OpenApiV31Deserializer.LoadSchemareturns anOpenApiSchemaReferenceimmediately on$ref(Reader/V31/OpenApiSchemaDeserializer.cs:452) and skipsParseMapentirely, so the fixed-field entries that recognize$defs/$dynamicAnchor/$id/$anchor/$vocabulary/$comment(lines 24-50) never execute.OpenApiSchemaReference's accessors then delegate toTarget, which is whyTarget's (absent) values surface instead.Screenshots/Code Snippets
The deeper impact is observable on the JSON Schema generics pattern (Pattern B from the
$dynamicRefecosystem). The fixture below is the canonical "reusable paginated response" pattern, validator-backed by Hyperjump:After parsing:
PaginatedUserResponseis anOpenApiSchemaReferencewithTarget = PaginatedTemplate.PaginatedUserResponse.DefinitionsreturnsPaginatedTemplate.Definitions(the placeholder slot), not the User-bound sibling.$defs.itemTypeentry. The dynamic-scope binding is unreachable.This blocks Kiota (and any other downstream tool using
Microsoft.OpenApi) from emittingclass PaginatedUserResponse : PaginatedTemplate<User>from this fixture.Additional context
Spec reference
$refis a regular applicator keyword; siblings are no longer ignored. The 3.0 "When a$refis used, all other properties SHALL be ignored" rule does not appear in 3.1.$dynamicAnchor/$dynamicRef/$defsare first-class keywords that participate in dynamic scope resolution alongside$ref.Why this matters
The JSON Schema generics pattern (a template schema with
$dynamicRefslots bound by sibling$defsin referencing schemas) is one of two primary real-world uses of$dynamicRef. It cannot be implemented on top ofMicrosoft.OpenApi3.7.0 because the binding information is dropped at parse time.A cross-generator compatibility matrix is tracked at https://github.com/aqeelat/openapi-dynamicref-adoption-tracker — multiple tools are affected by this pattern, and
Microsoft.OpenApiis the upstream blocker for all .NET-based tools (Kiota today; potentially others).Environment
Microsoft.OpenApi3.7.0Microsoft.OpenApi.YamlReader3.7.0Willingness to contribute a PR
I will open a PR soon.
This issue was drafted with assistance from AI tooling. The submitter is responsible for reviewing and validating the contents before submission.