Summary
When an OpenAPI contract is split across multiple files, a $ref that lives inside an externally-referenced file is resolved against the main openapi.yml directory instead of against the directory of the file that actually contains the $ref. As a result, nested cross-file references cannot be found and generation fails with FileNotFoundException.
Environment
- Plugin:
com.sngular:scs-multiapi-maven-plugin (reproduced on the 6.3.x/6.4.0 engine code)
- OpenAPI:
3.0/3.1, modular multi-file layout with external $ref
Reproduction
Layout:
api/rest/openapi.yml
api/rest/components/schemas/Service.yml
api/rest/components/schemas/ServiceType.yml
openapi.yml references components/schemas/Service.yml, and inside Service.yml:
service_type:
$ref: './ServiceType.yml'
Expected resolution: api/rest/components/schemas/ServiceType.yml (relative to Service.yml).
Actual: the plugin looks in the wrong directory (the rest/ segment is dropped / it resolves relative to the main file) and throws:
com.sngular.api.generator.plugin.openapi.exception.FileParseException: Failure trying to parse file Error reading api file
at com.sngular.api.generator.plugin.common.tools.SchemaUtil.readFile (SchemaUtil.java:98)
at com.sngular.api.generator.plugin.common.tools.SchemaUtil.getPojoFromRef (SchemaUtil.java:52)
at com.sngular.api.generator.plugin.common.tools.SchemaUtil.solveRef (SchemaUtil.java:37)
at com.sngular.api.generator.plugin.openapi.utils.MapperPathUtil.getRefSchema (MapperPathUtil.java:428)
...
Caused by: java.io.FileNotFoundException: .../api/components/schemas/ServiceType.yml (No such file or directory)
Root cause
The base URI used to resolve a nested ref is always derived from the main spec file, not from the referring file. Both resolution sites hard-code the main file's parent directory:
MapperPathUtil.getRefSchema(...) — every branch passes baseDir.resolve(specFile.getFilePath()).getParent().toUri() to SchemaUtil.solveRef.
ModelBuilder.solveRef(...) — same expression baseDir.resolve(specFile.getFilePath()).getParent().toUri().
Because a resolved JsonNode carries no provenance (the directory it was loaded from), any relative $ref it contains is later resolved against the root spec directory.
Suggested fix
Resolve a nested $ref relative to the referring file's directory. A localized, general approach is to rewrite relative external $ref values to absolute paths at file-load time in SchemaUtil.getPojoFromRef(...) (using the loaded file's own directory), so later resolution against any base still points at the correct file. This fixes both call sites at once.
Notes
No existing test exercises a nested cross-directory $ref (a $ref inside an external file pointing to another external file); the current multi-file fixtures (testExternalRefsGeneration, testReferenceFile) only use a single external components file. A reproduction fixture will be added with the fix.
Summary
When an OpenAPI contract is split across multiple files, a
$refthat lives inside an externally-referenced file is resolved against the mainopenapi.ymldirectory instead of against the directory of the file that actually contains the$ref. As a result, nested cross-file references cannot be found and generation fails withFileNotFoundException.Environment
com.sngular:scs-multiapi-maven-plugin(reproduced on the6.3.x/6.4.0engine code)3.0/3.1, modular multi-file layout with external$refReproduction
Layout:
openapi.ymlreferencescomponents/schemas/Service.yml, and insideService.yml:Expected resolution:
api/rest/components/schemas/ServiceType.yml(relative toService.yml).Actual: the plugin looks in the wrong directory (the
rest/segment is dropped / it resolves relative to the main file) and throws:Root cause
The base URI used to resolve a nested ref is always derived from the main spec file, not from the referring file. Both resolution sites hard-code the main file's parent directory:
MapperPathUtil.getRefSchema(...)— every branch passesbaseDir.resolve(specFile.getFilePath()).getParent().toUri()toSchemaUtil.solveRef.ModelBuilder.solveRef(...)— same expressionbaseDir.resolve(specFile.getFilePath()).getParent().toUri().Because a resolved
JsonNodecarries no provenance (the directory it was loaded from), any relative$refit contains is later resolved against the root spec directory.Suggested fix
Resolve a nested
$refrelative to the referring file's directory. A localized, general approach is to rewrite relative external$refvalues to absolute paths at file-load time inSchemaUtil.getPojoFromRef(...)(using the loaded file's own directory), so later resolution against any base still points at the correct file. This fixes both call sites at once.Notes
No existing test exercises a nested cross-directory
$ref(a$refinside an external file pointing to another external file); the current multi-file fixtures (testExternalRefsGeneration,testReferenceFile) only use a single external components file. A reproduction fixture will be added with the fix.