Skip to content

OpenAPI 3.0: schema $ref to a component's property (#/components/schemas/User/properties/name) is truncated to #/components/schemas/name #3154

Open

Description

@hidingmyname

Describe the bug

Location (microsoft/OpenAPI.NET @ b0a6859):

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:

  • Target is null when there is no top-level name schema, 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 an integer component instead of the string property.

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:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions