From 65a8ffb35553b9cd0febbf9cb81fd41690bc1097 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Wed, 26 Aug 2026 09:50:16 +0200 Subject: [PATCH 1/2] fix: Fix resolution of external refs inside path items with templated fragments (#2033) --- .../processors/ExternalRefProcessor.java | 6 +- .../swagger/v3/parser/test/Issue2033Test.java | 67 +++++++++++++++++++ .../issue-2033/common-spec-openapi.yaml | 17 +++++ .../src/test/resources/issue-2033/main.yaml | 7 ++ .../issue-2033/traceability/nodes.yaml | 24 +++++++ 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2033Test.java create mode 100644 modules/swagger-parser-v3/src/test/resources/issue-2033/common-spec-openapi.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/issue-2033/main.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/issue-2033/traceability/nodes.yaml diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java index 7d549985a1..2914ea7eb4 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java @@ -870,7 +870,7 @@ private void processRefExamples(Map examples, String $ref) { if (example.get$ref() != null) { RefFormat ref = computeRefFormat(example.get$ref()); if (isAnExternalRefFormat(ref)) { - processRefExample(example, $ref); + processRefExample(example, file); } else { processRefToExternalExample(file + example.get$ref(), RefFormat.RELATIVE); } @@ -917,7 +917,7 @@ private void processRefHeaders(Map headers, String $ref) { if (header.get$ref() != null) { RefFormat ref = computeRefFormat(header.get$ref()); if (isAnExternalRefFormat(ref)) { - processRefHeader(header, $ref); + processRefHeader(header, file); } else { processRefToExternalHeader(file + header.get$ref(), RefFormat.RELATIVE); } @@ -931,7 +931,7 @@ private void processRefLinks(Map links, String $ref) { if (link.get$ref() != null) { RefFormat ref = computeRefFormat(link.get$ref()); if (isAnExternalRefFormat(ref)) { - processRefLink(link, $ref); + processRefLink(link, file); } else { processRefToExternalLink(file + link.get$ref(), RefFormat.RELATIVE); } diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2033Test.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2033Test.java new file mode 100644 index 0000000000..e7dc0cebe9 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2033Test.java @@ -0,0 +1,67 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.links.Link; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class Issue2033Test { + + @Test + public void resolvesRelativeComponentRefsFromExternalTemplatedPathItem() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + + SwaggerParseResult result = new OpenAPIV3Parser().readLocation( + "src/test/resources/issue-2033/main.yaml", null, options); + + assertNotNull(result.getOpenAPI()); + assertTrue(result.getMessages().isEmpty(), "Unexpected parser messages: " + result.getMessages()); + + OpenAPI openAPI = result.getOpenAPI(); + ApiResponse partialResponse = openAPI.getPaths() + .get("/nodes/{uuid}/rights") + .getGet() + .getResponses() + .get("206"); + + Header contentRange = partialResponse.getHeaders().get("Content-Range"); + + assertNotNull(contentRange); + assertEquals(contentRange.get$ref(), "#/components/headers/Content-Range"); + + Header resolvedContentRange = openAPI.getComponents().getHeaders().get("Content-Range"); + assertNotNull(resolvedContentRange); + assertNotNull(resolvedContentRange.getSchema()); + assertEquals(resolvedContentRange.getSchema().getType(), "string"); + assertEquals(resolvedContentRange.getSchema().getPattern(), "\\d+-\\d+/\\d+"); + + Link nextPage = partialResponse.getLinks().get("nextPage"); + assertNotNull(nextPage); + assertEquals(nextPage.get$ref(), "#/components/links/NextPage"); + + Link resolvedNextPage = openAPI.getComponents().getLinks().get("NextPage"); + assertNotNull(resolvedNextPage); + assertEquals(resolvedNextPage.getOperationId(), "listNodeRights"); + + Example contentRangeExample = partialResponse.getContent() + .get("application/json") + .getExamples() + .get("contentRange"); + assertNotNull(contentRangeExample); + assertEquals(contentRangeExample.get$ref(), "#/components/examples/ContentRangeExample"); + + Example resolvedExample = openAPI.getComponents().getExamples().get("ContentRangeExample"); + assertNotNull(resolvedExample); + assertEquals(resolvedExample.getValue(), "items 0-9/42"); + } +} diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2033/common-spec-openapi.yaml b/modules/swagger-parser-v3/src/test/resources/issue-2033/common-spec-openapi.yaml new file mode 100644 index 0000000000..776666ec19 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2033/common-spec-openapi.yaml @@ -0,0 +1,17 @@ +openapi: 3.0.3 +info: + title: Common components + version: 1.0.0 +paths: {} +components: + examples: + ContentRangeExample: + value: items 0-9/42 + headers: + Content-Range: + schema: + type: string + pattern: '\d+-\d+/\d+' + links: + NextPage: + operationId: listNodeRights diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2033/main.yaml b/modules/swagger-parser-v3/src/test/resources/issue-2033/main.yaml new file mode 100644 index 0000000000..af82696c34 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2033/main.yaml @@ -0,0 +1,7 @@ +openapi: 3.0.3 +info: + title: Issue 2033 + version: 1.0.0 +paths: + /nodes/{uuid}/rights: + $ref: "traceability/nodes.yaml#/paths/~1nodes~1{uuid}~1rights" diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2033/traceability/nodes.yaml b/modules/swagger-parser-v3/src/test/resources/issue-2033/traceability/nodes.yaml new file mode 100644 index 0000000000..ee6fa2430f --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2033/traceability/nodes.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.3 +info: + title: Nodes + version: 1.0.0 +paths: + /nodes/{uuid}/rights: + get: + operationId: listNodeRights + responses: + "206": + description: Partial + content: + application/json: + schema: + type: string + examples: + contentRange: + $ref: "../common-spec-openapi.yaml#/components/examples/ContentRangeExample" + headers: + Content-Range: + $ref: "../common-spec-openapi.yaml#/components/headers/Content-Range" + links: + nextPage: + $ref: "../common-spec-openapi.yaml#/components/links/NextPage" From 389f3522c9adabd937db7cea9b5ea07485ce421a Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Wed, 26 Aug 2026 09:57:03 +0200 Subject: [PATCH 2/2] refactor processRef into a separate method --- .../processors/ExternalRefProcessor.java | 80 +++++-------------- 1 file changed, 22 insertions(+), 58 deletions(-) diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java index 2914ea7eb4..7556152fb1 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java @@ -10,6 +10,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -879,22 +881,8 @@ private void processRefExamples(Map examples, String $ref) { } private void processRefExample(Example example, String externalFile) { - RefFormat format = computeRefFormat(example.get$ref()); - - if (!isAnExternalRefFormat(format)) { - example.set$ref(RefType.SCHEMAS.getInternalPrefix()+ processRefToExternalSchema(externalFile + example.get$ref(), RefFormat.RELATIVE)); - return; - } - String $ref = example.get$ref(); - String subRefExternalPath = getExternalPath(example.get$ref()) - .orElse(null); - - if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { - $ref = join(externalFile, example.get$ref()); - example.set$ref($ref); - }else { - processRefToExternalExample($ref, format); - } + processRef(example.get$ref(), externalFile, example::set$ref, this::processRefToExternalExample, + ref -> join(externalFile, ref)); } private void processRefSchemaObject(Schema schema, String $ref) { @@ -942,65 +930,41 @@ private void processRefLinks(Map links, String $ref) { private void processRefSchema(Schema subRef, String externalFile) { - RefFormat format = computeRefFormat(subRef.get$ref()); - - if (!isAnExternalRefFormat(format)) { - subRef.set$ref(RefType.SCHEMAS.getInternalPrefix()+ processRefToExternalSchema(externalFile + subRef.get$ref(), RefFormat.RELATIVE)); - return; - } - String $ref = subRef.get$ref(); - String subRefExternalPath = getExternalPath(subRef.get$ref()) - .orElse(null); - - if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { - $ref = constructRef(subRef, externalFile); - subRef.set$ref($ref); - }else { - processRefToExternalSchema($ref, format); - } + processRef(subRef.get$ref(), externalFile, subRef::set$ref, this::processRefToExternalSchema, + ref -> constructRef(subRef, externalFile)); } - protected String constructRef(Schema refProperty, String rootLocation) { String ref = refProperty.get$ref(); return join(rootLocation, ref); } private void processRefHeader(Header subRef, String externalFile) { - RefFormat format = computeRefFormat(subRef.get$ref()); - - if (!isAnExternalRefFormat(format)) { - subRef.set$ref(RefType.SCHEMAS.getInternalPrefix()+ processRefToExternalSchema(externalFile + subRef.get$ref(), RefFormat.RELATIVE)); - return; - } - String $ref = subRef.get$ref(); - String subRefExternalPath = getExternalPath(subRef.get$ref()) - .orElse(null); - - if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { - $ref = join(externalFile, subRef.get$ref()); - subRef.set$ref($ref); - }else { - processRefToExternalHeader($ref, format); - } + processRef(subRef.get$ref(), externalFile, subRef::set$ref, this::processRefToExternalHeader, + ref -> join(externalFile, ref)); } private void processRefLink(Link subRef, String externalFile) { - RefFormat format = computeRefFormat(subRef.get$ref()); + processRef(subRef.get$ref(), externalFile, subRef::set$ref, this::processRefToExternalLink, + ref -> join(externalFile, ref)); + } + + private void processRef(String ref, String externalFile, Consumer refSetter, + BiConsumer externalRefProcessor, + Function relativeRefResolver) { + RefFormat format = computeRefFormat(ref); if (!isAnExternalRefFormat(format)) { - subRef.set$ref(RefType.SCHEMAS.getInternalPrefix()+ processRefToExternalSchema(externalFile + subRef.get$ref(), RefFormat.RELATIVE)); + refSetter.accept(RefType.SCHEMAS.getInternalPrefix() + + processRefToExternalSchema(externalFile + ref, RefFormat.RELATIVE)); return; } - String $ref = subRef.get$ref(); - String subRefExternalPath = getExternalPath(subRef.get$ref()) - .orElse(null); + String subRefExternalPath = getExternalPath(ref).orElse(null); if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { - $ref = join(externalFile, subRef.get$ref()); - subRef.set$ref($ref); - }else { - processRefToExternalLink($ref, format); + refSetter.accept(relativeRefResolver.apply(ref)); + } else { + externalRefProcessor.accept(ref, format); } }