Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.parser.util.DeserializationUtils;
import io.swagger.v3.parser.util.OpenAPIDeserializer;
import io.swagger.v3.parser.util.RefUtils;
import io.swagger.v3.parser.models.RefFormat;
import org.apache.commons.lang3.StringUtils;

import static io.swagger.v3.parser.util.RefUtils.computeRefFormat;
import static io.swagger.v3.parser.util.RefUtils.isAnExternalRefFormat;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -57,6 +62,7 @@ public OpenAPI31Traverser context(DereferencerContext context) {

public Set<Object> visiting = new HashSet<>();
protected HashMap<Object, Object> visitedMap = new HashMap<>();
protected Map<String, String> processedExternalSchemaRefs = new HashMap<>();

public OpenAPI traverse(OpenAPI openAPI, Visitor visitor) throws Exception {
if (!(visitor instanceof ReferenceVisitor)) {
Expand Down Expand Up @@ -920,6 +926,19 @@ public Schema traverseSchema(Schema schema, ReferenceVisitor visitor, List<Strin
visiting.remove(schema);
return handleRootLocalRefs(schema.get$ref(), resolved, context.getOpenApi().getComponents().getSchemas());
}
// handle local refs within external files - promote to components.schemas
// This mirrors ExternalRefProcessor.processRefSchema() from the 3.0 code path
if (shouldHandleLocalRefInExternalFile(resolvedNotNull, schema.get$ref(), visitor)) {
String cacheKey = visitor.reference.getUri() + schema.get$ref();
String baseName = ReferenceUtils.getRefName(schema.get$ref());
return promoteSchemaToComponents(schema, resolved, inheritedIds, cacheKey, baseName);
}
// handle external schema refs - add to components.schemas and return $ref
if (shouldHandleExternalSchemaRef(resolvedNotNull, schema.get$ref())) {
String cacheKey = schema.get$ref();
String baseName = computeExternalSchemaName(schema.get$ref());
return promoteSchemaToComponents(schema, resolved, inheritedIds, cacheKey, baseName);
}
// merge ALL STUFF
mergeSchemas(schema, resolved);
visitedMap.put(schema, deepcopy(resolved, Schema.class));
Expand Down Expand Up @@ -983,6 +1002,106 @@ public boolean shouldHandleRootLocalRefs(boolean resolvedNotNull, String ref, Re
(ReferenceUtils.isLocalRefToComponents(ref) || ReferenceUtils.isAnchorRef(ref));
}

public boolean isExternalRef(String ref) {
if (StringUtils.isBlank(ref)) {
return false;
}
RefFormat refFormat = computeRefFormat(ref);
return isAnExternalRefFormat(refFormat);
}

public boolean shouldHandleExternalSchemaRef(boolean resolvedNotNull, String ref) {
return resolvedNotNull && isExternalRef(ref);
}

public boolean shouldHandleLocalRefInExternalFile(boolean resolvedNotNull, String ref, ReferenceVisitor visitor) {
return resolvedNotNull &&
ReferenceUtils.isLocalRef(ref) &&
!visitor.reference.getUri().equals(this.getContext().getRootUri());
}

private void finalizeSchemaVisit(Schema schema, Schema resolved, List<String> inheritedIds) {
visitedMap.put(schema, deepcopy(resolved, Schema.class));
visiting.remove(schema);
if (StringUtils.isNotBlank(schema.get$id())) {
inheritedIds.remove(schema.get$id());
}
}

/**
* Computes the component name for an external schema ref.
* For anchor-style refs (e.g., ./ex.json#user-profile), uses the anchor value as the name.
* This is 3.1-specific since $anchor is a JSON Schema 2020-12 feature.
* For JSON pointer refs (e.g., ./ex.json#/path/to/Schema), falls back to RefUtils.computeDefinitionName.
*/
private String computeExternalSchemaName(String ref) {
int hashIndex = ref.indexOf('#');
if (hashIndex >= 0 && hashIndex < ref.length() - 1) {
String fragment = ref.substring(hashIndex + 1);
// Anchor refs don't start with '/' (JSON pointers do)
if (!fragment.startsWith("/")) {
return fragment;
}
}
return RefUtils.computeDefinitionName(ref);
}

/**
* Promotes a resolved schema to components.schemas and returns either the resolved schema
* (resolveFully) or a $ref pointing to the promoted component.
*/
private Schema promoteSchemaToComponents(Schema schema, Schema resolved, List<String> inheritedIds,
String cacheKey, String baseName) {
String refName;
if (processedExternalSchemaRefs.containsKey(cacheKey)) {
refName = processedExternalSchemaRefs.get(cacheKey);
} else {
mergeSchemas(schema, resolved);
ensureComponents(context.getOpenApi());
if (context.getOpenApi().getComponents().getSchemas() == null) {
context.getOpenApi().getComponents().schemas(new LinkedHashMap<>());
}
Map<String, Schema> schemasMap = context.getOpenApi().getComponents().getSchemas();
refName = getUniqueSchemaName(schemasMap, baseName, resolved);
schemasMap.put(refName, resolved);
processedExternalSchemaRefs.put(cacheKey, refName);
}

finalizeSchemaVisit(schema, resolved, inheritedIds);

if (this.getContext().getParseOptions().isResolveFully()) {
return resolved;
} else {
Schema refSchema = new Schema();
refSchema.set$ref("#/components/schemas/" + refName);
return refSchema;
}
}

private String getUniqueSchemaName(Map<String, Schema> schemas, String name, Schema schema) {
String uniqueName = name;
int counter = 0;
while (schemas.containsKey(uniqueName)) {
Schema existingSchema = schemas.get(uniqueName);
if (schemasAreEqual(existingSchema, schema)) {
return uniqueName;
}
counter++;
uniqueName = name + "_" + counter;
}
return uniqueName;
}

private boolean schemasAreEqual(Schema s1, Schema s2) {
try {
String json1 = Json31.mapper().writeValueAsString(s1);
String json2 = Json31.mapper().writeValueAsString(s2);
return json1.equals(json2);
} catch (JsonProcessingException e) {
return false;
}
}

public void ensureComponents(OpenAPI openAPI) {
if (openAPI.getComponents() == null) {
openAPI.setComponents(new Components());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.swagger.v3.parser.test;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
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 java.io.File;
import java.util.Map;

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
* Test for GitHub issue #2266: External schema resolution broken in OpenAPI 3.1 (works in 3.0)
*
* When parsing an OpenAPI 3.1 specification with external schema references (using $ref to separate YAML files),
* the getComponents().getSchemas() method should return the resolved schemas, not null.
*/
public class OpenAPIV31ParserExternalSchemaRefTest {

@Test
public void testExternalSchemaRefResolvedToComponents() throws Exception {
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);

SwaggerParseResult result = new OpenAPIV3Parser().readLocation(
new File("src/test/resources/3.1.0/dereference/external-schema-ref/swagger.yaml").getAbsolutePath(),
null,
parseOptions
);

OpenAPI openAPI = result.getOpenAPI();

assertNotNull(openAPI, "OpenAPI should not be null");
assertNotNull(openAPI.getComponents(), "Components should not be null");
assertNotNull(openAPI.getComponents().getSchemas(), "Schemas should not be null");

Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
assertTrue(schemas.size() > 0, "Schemas should contain at least one entry");
assertTrue(schemas.containsKey("ProbeInfo"), "Schemas should contain 'ProbeInfo'");

Schema probeInfoSchema = schemas.get("ProbeInfo");
assertNotNull(probeInfoSchema, "ProbeInfo schema should not be null");
assertNotNull(probeInfoSchema.getProperties(), "ProbeInfo properties should not be null");
assertTrue(probeInfoSchema.getProperties().containsKey("status"), "ProbeInfo should have 'status' property");
assertTrue(probeInfoSchema.getProperties().containsKey("version"), "ProbeInfo should have 'version' property");
}

@Test
public void testExternalSchemaRefResolvedFullyToComponents() throws Exception {
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveFully(true);

SwaggerParseResult result = new OpenAPIV3Parser().readLocation(
new File("src/test/resources/3.1.0/dereference/external-schema-ref/swagger.yaml").getAbsolutePath(),
null,
parseOptions
);

OpenAPI openAPI = result.getOpenAPI();

assertNotNull(openAPI, "OpenAPI should not be null");
assertNotNull(openAPI.getComponents(), "Components should not be null");
assertNotNull(openAPI.getComponents().getSchemas(), "Schemas should not be null with resolveFully");

Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
assertTrue(schemas.size() > 0, "Schemas should contain at least one entry");
assertTrue(schemas.containsKey("ProbeInfo"), "Schemas should contain 'ProbeInfo'");
}
}
Loading