Repository navigation
Description
Describe the bug
Location (microsoft/OpenAPI.NET @ b0a6859):
src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs:391-401: the 3.0 reader builds the schema reference from the last segment of the pointer only.src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs:153-166:GetReferenceIdAndExternalResourcereturnsrefSegments[refSegments.Length - 1]as the id.src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs:435-453: the 3.1 reader also keeps the full pointer withSetJsonPointerPath, which the 3.0 reader never calls.
public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocument, ParsingContext context)
{
var jsonObject = node.CheckMapNode(OpenApiConstants.Schema, context);
var pointer = jsonObject.GetReferencePointer();
if (pointer != null)
{
var reference = GetReferenceIdAndExternalResource(pointer);
return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
}Problem
In an OpenAPI 3.0 document, $ref: '#/components/schemas/User/properties/name' becomes a reference with id name and ReferenceV3 #/components/schemas/name. The rest of the path is lost, so:
Targetisnullwhen there is no top-levelnameschema, and- if the document also has a component called
name, the reference silently resolves to that unrelated schema. In the test below it resolves to anintegercomponent instead of thestringproperty.
References into component sub-schemas were requested in #2144, whose example is an openapi: 3.0.0 document. That issue was closed as supported in v2, and 3.1 documents do resolve such references (V31Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml). The 3.0 reader still truncates them. Nested property paths (.../properties/address/properties/city) behave the same way. I only tested internal references. External-file sub-schema references in 3.0 were not exercised.
OpenApi File To Reproduce
openapi: 3.0.3
info:
title: Subschema references
version: 1.0.0
paths: {}
components:
schemas:
User:
type: object
properties:
name:
type: string
address:
type: object
properties:
city:
type: string
name:
type: integer
UserName:
$ref: '#/components/schemas/User/properties/name'
UserCity:
$ref: '#/components/schemas/User/properties/address/properties/city'Expected behavior
UserName resolves to the string schema at User/properties/name and keeps #/components/schemas/User/properties/name as its ReferenceV3; UserCity resolves to the string schema at User/properties/address/properties/city, the same as for an openapi: 3.1.0 document.
Screenshots/Code Snippets
test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSubschemaReferenceTests.cs (document above inlined as `Document`)
[Fact]
public void ReferenceToComponentPropertyResolvesToThatProperty()
{
var document = OpenApiDocument.Parse(Document, "yaml", SettingsFixture.ReaderSettings).Document;
var userName = Assert.IsType<OpenApiSchemaReference>(document.Components.Schemas["UserName"]);
// The document also has a top-level "name" component (an integer); it must not be picked instead.
Assert.NotNull(userName.Target);
Assert.Equal(JsonSchemaType.String, userName.Target.Type);
Assert.Equal("#/components/schemas/User/properties/name", userName.Reference.ReferenceV3);
}
[Fact]
public void ReferenceToNestedComponentPropertyResolvesToThatProperty()
{
var document = OpenApiDocument.Parse(Document, "yaml", SettingsFixture.ReaderSettings).Document;
var userCity = Assert.IsType<OpenApiSchemaReference>(document.Components.Schemas["UserCity"]);
Assert.NotNull(userCity.Target);
Assert.Equal(JsonSchemaType.String, userCity.Target.Type);
Assert.Equal("#/components/schemas/User/properties/address/properties/city", userCity.Reference.ReferenceV3);
}Output:
failed Microsoft.OpenApi.Readers.Tests.V3Tests.OpenApiSubschemaReferenceTests.ReferenceToComponentPropertyResolvesToThatProperty (1ms)
Assert.Equal() Failure: Values differ
Expected: String
Actual: Integer
failed Microsoft.OpenApi.Readers.Tests.V3Tests.OpenApiSubschemaReferenceTests.ReferenceToNestedComponentPropertyResolvesToThatProperty (1ms)
Assert.NotNull() Failure: Value is null
With the ReferenceV3 assertions checked first, the values are:
Expected: "#/components/schemas/User/properties/name"
Actual: "#/components/schemas/name"
...
Expected: "#/components/schemas/User/properties/address/prope"···
Actual: "#/components/schemas/city"
Environment:
- Commit:
b0a6859be48927746726e967f578f370cd756f35(version 3.11.0);LoadSchemais unchanged onmainat47a130b - Toolchain: .NET SDK 10.0.403, net10.0, Linux x64
Additional context
Already fixed in hey-api/hey-api
PR: hey-api/hey-api#2020 ("fix: enhance ensureUniqueIdentifier to handle properties in $ref", merged 2025-05-07T23:16:58Z, merge commit 930a8d2e9f0). hey-api's OpenAPI code generator made the same mistake of naming a $ref after its last path segment. It now splits #/components/schemas/User/properties/name into the base component and the property path. hey-api is an independent tool for the same specification, so this is only a precedent.
How hey-api fixed it (relevant hunks, verbatim):
--- a/packages/openapi-ts/src/generate/files.ts
+++ b/packages/openapi-ts/src/generate/files.ts
@@ -360,8 +394,7 @@ const ensureUniqueIdentifier = ({
nameTransformer,
namespace,
}: EnsureUniqueIdentifierData): Identifier => {
- const parts = $ref.split('/');
- const name = parts[parts.length - 1] || '';
+ const { baseRef, name, properties } = parseRefPath($ref);
if (!name) {
return {
@@ -370,11 +403,15 @@ const ensureUniqueIdentifier = ({
};
}
- const refValue = namespace[$ref];
+ const refValue = namespace[baseRef];
if (refValue) {
+ let name = refValue.name;
+ if (properties.length) {
+ name += properties.map((property) => `['${property}']`).join('');
+ }
return {
created: false,
- name: refValue.name,
+ name: name as string,
};
}
Suggested fix
Do in the 3.0 reader what the 3.1/3.2 readers already do, and record the full JSON pointer on the reference:
diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
--- a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
@@ -397,7 +397,9 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
if (pointer != null)
{
var reference = GetReferenceIdAndExternalResource(pointer);
- return new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
+ var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
+ result.Reference.SetJsonPointerPath(pointer, context.GetLocation());
+ return result;
}
var schema = new OpenApiSchema();Verified on b0a6859 (.NET SDK 10.0.403, net10.0): the tests above fail without this patch and pass with it; no other test results change in Microsoft.OpenApi.Readers.Tests or Microsoft.OpenApi.Tests (1310 tests). The only other failure, OpenApiStreamReaderTests.StreamShouldReadWhenInitializedAsync, needs network access and fails the same way with or without the patch in my environment.
I can submit a PR for this.
Related: #2144 asked for exactly this kind of $ref with a 3.0.0 document and was closed as supported in v2. #3148 and #3153 deal with relative $refs without a fragment in 3.1. I found no open issue about 3.0 sub-schema references.
Activity