fix(openapi-yaml): localize nested multi-file component $refs - #24540
fix(openapi-yaml): localize nested multi-file component $refs#24540arimu1 wants to merge 1 commit into
Conversation
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
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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>
| 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) { |
There was a problem hiding this comment.
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"), |
There was a problem hiding this comment.
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>
| Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/schemas/ComplexType"), | |
| Assert.assertFalse(generatedYaml.contains("swagger.yml#/components/"), |
Description
When generating with
-g openapi-yamlfrom a multi-file OpenAPI document, nested$refs that point back into the root document (for example fromschemas/ArrayOfComplexTypes.yml→../swagger.yml#/components/schemas/ComplexType) can remain as relative file paths in the bundled output:instead of the expected internal form:
That makes the generated single-file YAML invalid for downstream tooling and surfaces
Failed to get the schema name/Error obtaining the datatype from refwarnings during generation.This change rewrites external-looking component
$refs to#/components/...when the target already exists in local components, duringopenapi-yamlpreprocessing.Fixes #24528
Testing
modules/openapi-generator/src/test/resources/3_0/issue_24528/matching the issue reproduction.YamlGeneratorTest#testIssue24528NestedExternalRefsLocalized../mvnw -pl modules/openapi-generator test -Dtest=YamlGeneratorTest→ 7/7 passedArrayOfComplexTypes/NullableComplexTypenow emit#/components/schemas/ComplexType.Platform: macOS aarch64, JDK 17.
PR checklist
openapi-yamlgenerator and unit tests; no sample configs/templates were modified.)(Documentation generator / core; no language TC mention required.)
Summary by cubic
Fixes nested multi-file component $refs in the
openapi-yamlgenerator 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.Written for commit 5de7091. Summary will update on new commits.