Skip to content

fix(openapi-yaml): localize nested multi-file component $refs - #24540

Open
arimu1 wants to merge 1 commit into
OpenAPITools:masterfrom
arimu1:fix/24528-openapi-yaml-nested-refs
Open

fix(openapi-yaml): localize nested multi-file component $refs#24540
arimu1 wants to merge 1 commit into
OpenAPITools:masterfrom
arimu1:fix/24528-openapi-yaml-nested-refs

Conversation

@arimu1

@arimu1 arimu1 commented Jul 31, 2026

Copy link
Copy Markdown

Description

When generating with -g openapi-yaml from a multi-file OpenAPI document, nested $refs that point back into the root document (for example from schemas/ArrayOfComplexTypes.yml../swagger.yml#/components/schemas/ComplexType) can remain as relative file paths in the bundled output:

$ref: ./swagger.yml#/components/schemas/ComplexType

instead of the expected internal form:

$ref: "#/components/schemas/ComplexType"

That makes the generated single-file YAML invalid for downstream tooling and surfaces Failed to get the schema name / Error obtaining the datatype from ref warnings during generation.

This change rewrites external-looking component $refs to #/components/... when the target already exists in local components, during openapi-yaml preprocessing.

Fixes #24528

Testing

  • Added multi-file fixture under modules/openapi-generator/src/test/resources/3_0/issue_24528/ matching the issue reproduction.
  • Added YamlGeneratorTest#testIssue24528NestedExternalRefsLocalized.
  • Ran: ./mvnw -pl modules/openapi-generator test -Dtest=YamlGeneratorTest7/7 passed
  • Manually regenerated the issue fixture with the patched classes; ArrayOfComplexTypes / NullableComplexType now emit #/components/schemas/ComplexType.

Platform: macOS aarch64, JDK 17.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (Not run: change is limited to the openapi-yaml generator and unit tests; no sample configs/templates were modified.)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
    (Documentation generator / core; no language TC mention required.)

Summary by cubic

Fixes nested multi-file component $refs in the openapi-yaml generator by rewriting external-looking refs (e.g. ./swagger.yml#/...) to internal #/components/... when targets exist, producing a valid single-file YAML and removing warnings. Fixes #24528.

  • Bug Fixes
    • Localizes component $refs during preprocessing and traverses schema trees (properties, arrays, additionalProperties, allOf/anyOf/oneOf/not) to rewrite eligible refs.
    • Adds unit test and multi-file fixtures to cover the regression.

Written for commit 5de7091. Summary will update on new commits.

Review in cubic

When multi-file specs are resolved, swagger-parser can leave nested $refs
as relative file paths into the root document (e.g.
./swagger.yml#/components/schemas/ComplexType). Rewrite those to internal
#/components/... refs when the target already exists in local components
so the bundled openapi.yaml is self-contained and valid.

Fixes OpenAPITools#24528

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/yaml/YamlGeneratorTest.java">

<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/yaml/YamlGeneratorTest.java:267">
P3: The regression test can pass while other nested refs remain external. The `ComplexType.yml` fixture contains refs to `SimpleType`, but this assertion only rejects the `ComplexType` suffix; an implementation that rewrites only the `ComplexType` refs would satisfy both this assertion and the later `ComplexType` checks while leaving all `SimpleType` refs unchanged. Broadening the assertion to reject any `swagger.yml#/components/` ref (or explicitly asserting the three `ComplexType` properties) would cover the full fixture.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java:138">
P1: External component refs outside `components.schemas` are never visited, so the generated single-file document can still contain the exact invalid-looking refs this change is intended to remove. The walker is started only for schema components; it does not inspect schemas in path operations or in component parameters, responses, request bodies, headers, callbacks, or path items. The existing `InlineModelResolver` has dedicated traversal for those containers, which confirms they can carry schema refs. The localization pass should traverse all schema-bearing locations in the OpenAPI document, not just the top-level schema map.</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java:174">
P2: Refs nested in several schema keywords are still omitted from localization. The recursive walker handles properties, items, additionalProperties, allOf/anyOf/oneOf, and not, but not `patternProperties`, `dependentSchemas`, `prefixItems`, `if`/`then`/`else`, `contains`, or the other JSON Schema sub-schema fields supported by this codebase. A multi-file OpenAPI 3.1 schema using one of those fields can therefore still serialize an external-looking component ref.</violation>

<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java:205">
P1: A reference to a genuinely external document can be silently rebound to a same-named root component. This code keeps only the `#/components/...` suffix and then checks whether that name exists locally; it never verifies that the URI prefix identifies the input/root document. For example, `common.yml#/components/schemas/User` will become `#/components/schemas/User` whenever the root also defines `User`, changing the generated schema's meaning. The rewrite should be limited to refs proven to resolve to the root document, rather than to every external URI with a matching component name.</violation>

<violation number="4" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java:214">
P2: Component names containing a literal `+` are not localized. `URLDecoder.decode` turns `+` into a space, so `./swagger.yml#/components/schemas/A+B` is looked up as `A B`; when the actual local key is `A+B`, `componentExists` returns false and the external ref remains in the generated YAML. Fragment decoding should preserve literal plus characters.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
// Walk component schemas directly so we do not call getSimpleRef (and its warnings)
// on still-external nested refs before they are rewritten.
for (Schema<?> schema : openAPI.getComponents().getSchemas().values()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: External component refs outside components.schemas are never visited, so the generated single-file document can still contain the exact invalid-looking refs this change is intended to remove. The walker is started only for schema components; it does not inspect schemas in path operations or in component parameters, responses, request bodies, headers, callbacks, or path items. The existing InlineModelResolver has dedicated traversal for those containers, which confirms they can carry schema refs. The localization pass should traverse all schema-bearing locations in the OpenAPI document, not just the top-level schema map.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java, line 138:

<comment>External component refs outside `components.schemas` are never visited, so the generated single-file document can still contain the exact invalid-looking refs this change is intended to remove. The walker is started only for schema components; it does not inspect schemas in path operations or in component parameters, responses, request bodies, headers, callbacks, or path items. The existing `InlineModelResolver` has dedicated traversal for those containers, which confirms they can carry schema refs. The localization pass should traverse all schema-bearing locations in the OpenAPI document, not just the top-level schema map.</comment>

<file context>
@@ -98,13 +104,150 @@ public void processOpts() {
+        }
+        // Walk component schemas directly so we do not call getSimpleRef (and its warnings)
+        // on still-external nested refs before they are rewritten.
+        for (Schema<?> schema : openAPI.getComponents().getSchemas().values()) {
+            walkAndRewriteSchemaRefs(schema, openAPI);
+        }
</file context>

if (fragmentIndex < 0) {
return null;
}
String fragment = ref.substring(fragmentIndex);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A reference to a genuinely external document can be silently rebound to a same-named root component. This code keeps only the #/components/... suffix and then checks whether that name exists locally; it never verifies that the URI prefix identifies the input/root document. For example, common.yml#/components/schemas/User will become #/components/schemas/User whenever the root also defines User, changing the generated schema's meaning. The rewrite should be limited to refs proven to resolve to the root document, rather than to every external URI with a matching component name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java, line 205:

<comment>A reference to a genuinely external document can be silently rebound to a same-named root component. This code keeps only the `#/components/...` suffix and then checks whether that name exists locally; it never verifies that the URI prefix identifies the input/root document. For example, `common.yml#/components/schemas/User` will become `#/components/schemas/User` whenever the root also defines `User`, changing the generated schema's meaning. The rewrite should be limited to refs proven to resolve to the root document, rather than to every external URI with a matching component name.</comment>

<file context>
@@ -98,13 +104,150 @@ public void processOpts() {
+        if (fragmentIndex < 0) {
+            return null;
+        }
+        String fragment = ref.substring(fragmentIndex);
+        // fragment is "#/components/{section}/{name}[/...]" — strip "#/" before splitting
+        String path = fragment.startsWith("#/") ? fragment.substring(2) : fragment.substring(1);
</file context>

return null;
}
String section = parts[1];
String name = URLDecoder.decode(parts[2], StandardCharsets.UTF_8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Component names containing a literal + are not localized. URLDecoder.decode turns + into a space, so ./swagger.yml#/components/schemas/A+B is looked up as A B; when the actual local key is A+B, componentExists returns false and the external ref remains in the generated YAML. Fragment decoding should preserve literal plus characters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java, line 214:

<comment>Component names containing a literal `+` are not localized. `URLDecoder.decode` turns `+` into a space, so `./swagger.yml#/components/schemas/A+B` is looked up as `A B`; when the actual local key is `A+B`, `componentExists` returns false and the external ref remains in the generated YAML. Fragment decoding should preserve literal plus characters.</comment>

<file context>
@@ -98,13 +104,150 @@ public void processOpts() {
+            return null;
+        }
+        String section = parts[1];
+        String name = URLDecoder.decode(parts[2], StandardCharsets.UTF_8)
+                .replace("~1", "/")
+                .replace("~0", "~");
</file context>
Suggested change
String name = URLDecoder.decode(parts[2], StandardCharsets.UTF_8)
String name = URLDecoder.decode(parts[2].replace("+", "%2B"), StandardCharsets.UTF_8)

walkAndRewriteSchemaRefs((Schema<?>) s, openAPI);
}
}
if (schema.getNot() != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Refs nested in several schema keywords are still omitted from localization. The recursive walker handles properties, items, additionalProperties, allOf/anyOf/oneOf, and not, but not patternProperties, dependentSchemas, prefixItems, if/then/else, contains, or the other JSON Schema sub-schema fields supported by this codebase. A multi-file OpenAPI 3.1 schema using one of those fields can therefore still serialize an external-looking component ref.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OpenAPIYamlGenerator.java, line 174:

<comment>Refs nested in several schema keywords are still omitted from localization. The recursive walker handles properties, items, additionalProperties, allOf/anyOf/oneOf, and not, but not `patternProperties`, `dependentSchemas`, `prefixItems`, `if`/`then`/`else`, `contains`, or the other JSON Schema sub-schema fields supported by this codebase. A multi-file OpenAPI 3.1 schema using one of those fields can therefore still serialize an external-looking component ref.</comment>

<file context>
@@ -98,13 +104,150 @@ public void processOpts() {
+                walkAndRewriteSchemaRefs((Schema<?>) s, openAPI);
+            }
+        }
+        if (schema.getNot() != null) {
+            walkAndRewriteSchemaRefs(schema.getNot(), openAPI);
+        }
</file context>

String generatedYaml = new String(Files.readAllBytes(generated), StandardCharsets.UTF_8);

// Nested multi-file $refs must be rewritten to internal component refs.
Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/schemas/ComplexType"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The regression test can pass while other nested refs remain external. The ComplexType.yml fixture contains refs to SimpleType, but this assertion only rejects the ComplexType suffix; an implementation that rewrites only the ComplexType refs would satisfy both this assertion and the later ComplexType checks while leaving all SimpleType refs unchanged. Broadening the assertion to reject any swagger.yml#/components/ ref (or explicitly asserting the three ComplexType properties) would cover the full fixture.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/yaml/YamlGeneratorTest.java, line 267:

<comment>The regression test can pass while other nested refs remain external. The `ComplexType.yml` fixture contains refs to `SimpleType`, but this assertion only rejects the `ComplexType` suffix; an implementation that rewrites only the `ComplexType` refs would satisfy both this assertion and the later `ComplexType` checks while leaving all `SimpleType` refs unchanged. Broadening the assertion to reject any `swagger.yml#/components/` ref (or explicitly asserting the three `ComplexType` properties) would cover the full fixture.</comment>

<file context>
@@ -243,4 +243,41 @@ public void testSortOutput() throws Exception {
+        String generatedYaml = new String(Files.readAllBytes(generated), StandardCharsets.UTF_8);
+
+        // Nested multi-file $refs must be rewritten to internal component refs.
+        Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/schemas/ComplexType"),
+                "Nested ComplexType $refs must not keep an external file path. Output was:\n" + generatedYaml);
+        Assert.assertTrue(generatedYaml.contains("#/components/schemas/ComplexType"),
</file context>
Suggested change
Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/schemas/ComplexType"),
Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/"),

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] openapi-yaml generator produces invalid output with nested references

1 participant