[kotlin-spring] feature: Add `x-extra-imports` vendor extension
The kotlin-spring generator already supports injecting custom annotations through the
x-class-extra-annotation, x-field-extra-annotation, and x-operation-extra-annotation
vendor extensions. However, those annotations had to be written using their fully-qualified
names, because there was no way to add the corresponding import statements to the generated
files.
This PR introduces a new x-extra-imports vendor extension that lets users declare the
imports needed by those custom annotations (or any other custom code), so annotations can be
referenced by their short name instead of being fully qualified.
Before
Widget:
x-class-extra-annotation: "@com.example.validation.ValidWidget"
After
Widget:
x-extra-imports:
- com.example.validation.ValidWidget
x-class-extra-annotation: "@ValidWidget"
Primary motivation: programmatic generation via custom normalizers
While the extension is useful when hand-authoring specs, its main value is in programmatic
workflows. A common pattern is to put your own domain-specific vendor extensions on the spec and
process them with a custom OpenAPINormalizer implementation that translates them into the
generator's extra-annotation extensions.
Until now, such a normalizer had no way to add the imports those annotations require, so it was
forced to emit fully-qualified annotations, resulting in verbose, hard-to-read generated
code. With x-extra-imports, the same normalizer can add both the (short-name) annotation and its
import, producing much cleaner output:
// Inside a custom OpenAPINormalizer, reacting to a custom `x-my-marker` extension:
schema.addExtension("x-extra-imports", List.of("com.example.validation.ValidWidget"));
schema.addExtension("x-class-extra-annotation", "@ValidWidget");
This keeps the annotation wiring and its import declaration together in the code that generates
them, instead of scattering fully-qualified names throughout the generated sources.
Worked example: a custom x-requires-role extension
Say your team annotates operations in the spec with a simple, domain-specific extension:
paths:
/accounts:
get:
operationId: listAccounts
x-requires-role: ADMIN
A custom OpenAPINormalizer can translate that single marker into a Spring Security
@PreAuthorize annotation together with the import it needs:
public class RoleNormalizer extends OpenAPINormalizer {
@Override
public Operation normalizeOperation(Operation operation) {
Object role = operation.getExtensions() == null
? null
: operation.getExtensions().get("x-requires-role");
if (role != null) {
operation.addExtension(
"x-operation-extra-annotation",
"@PreAuthorize(\"hasRole('" + role + "')\")");
operation.addExtension(
"x-extra-imports",
List.of("org.springframework.security.access.prepost.PreAuthorize"));
}
return super.normalizeOperation(operation);
}
}
The generated API interface then contains a clean, short-name annotation with its import already
in place:
import org.springframework.security.access.prepost.PreAuthorize
interface AccountsApi {
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(method = [RequestMethod.GET], value = ["/accounts"])
fun listAccounts(): ResponseEntity<List<Account>>
}
Without x-extra-imports, the normalizer would instead have to emit
@org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')") to avoid a compile
error, which is exactly the kind of noise this extension removes.
What it does
x-extra-imports accepts a single string or a list of strings and is supported at the
following levels:
| Level | Where to put it | Generated into |
|---|---|---|
| Model | On a schema | The model file's imports |
| Field | On a property schema | The owning model file's imports |
| Operation | On an operation | The API file's imports |
| Operation parameter | On a parameter | The API file's imports |
Key behaviors:
- Correct file-scoping. Imports are aggregated from the current model/operations group, so
only the imports relevant to a given generated file are emitted. This works correctly whether
operations are grouped by tags or by path segments (useTagson/off), and multi-tag
operations correctly contribute their imports to each generated API file. - Deduplication. Identical imports declared in multiple places are emitted once per file.
- Alias imports supported. Entries such as
com.example.other.Foo as Barare preserved
verbatim. - Values are passed through as-is. Blank/aliased/unusual values are not validated. The
Kotlin compiler remains the source of truth (consistent with how the existing
extra-annotation extensions behave). - No leakage into wrapper/delegate files. Only the templates that render the corresponding
annotations (api.mustache,apiInterface.mustache) render operation imports, so controller,
delegate, and service files are unaffected.
Implementation
VendorExtension.java: registered the newX_EXTRA_IMPORTSextension with levels
MODEL, FIELD, OPERATION, OPERATION_PARAMETER.KotlinSpringServerCodegen.java- Registered the extension in
getSupportedVendorExtensions(). - Added
postProcessModelsto collect model- and field-level imports into the model file's
imports. - Extended
postProcessOperationsWithModelsto aggregate operation- and parameter-level
imports intooperationExtraImportsfor the API file. - Import collection reuses the existing
DefaultCodegen.getObjectAsStringList(Object)helper,
the same mechanism used by the siblingx-*-extra-annotationextensions, so single-string
and list-of-strings inputs are handled identically.
- Registered the extension in
- Templates:
api.mustacheandapiInterface.mustacherenderoperationExtraImports
after the existing{{#imports}}block. - Docs: added the
x-extra-importsrow todocs/generators/kotlin-spring.md.
Testing
Added a fixture (kotlin-spring-extra-imports.yaml) and four unit tests in
KotlinSpringServerCodegenTest covering:
- model- and field-level imports, including deduplication and alias imports;
- API imports with tag grouping (including a multi-tag operation);
- API imports without tag grouping (path-segment grouping);
- isolation, verifying delegate/wrapper files do not receive annotation-only imports.
All four tests pass.
Notes
- No existing behavior is changed; the extension is purely additive and opt-in.
- Model-level imports work across all kotlin-spring libraries. Operation-level imports are
emitted for the libraries whose templates render operation annotations.
Future work: possible extension to other generators
This PR intentionally scopes x-extra-imports to kotlin-spring only. The same mechanism could
later be extended to other JVM generators with minimal new design, because they already meet the
two prerequisites this feature relies on:
- they support the sibling
x-*-extra-annotationextensions, and - their templates render an
{{#imports}}import {{import}}{{/imports}}block.
Candidates, in rough order of value:
- The Java family (all generators extending
AbstractJavaCodegen: Spring, JAX-RS, micronaut,
helidon, vertx, camel, the Java client libraries, etc.).Java/model.mustacheand
JavaSpring/api.mustachealready use the same{{#imports}}structure, so the logic could be
implemented once inAbstractJavaCodegen(model + field) andSpringCodegen(operation +
parameter) and be inherited broadly. kotlin-client, which sharesAbstractKotlinCodegenwith kotlin-spring and whose
model.mustacherenders the same imports block. The model-level logic added here could be lifted
intoAbstractKotlinCodegenand shared between the two Kotlin generators.
Non-JVM generators (Python, TypeScript, Go, PHP, Ruby, etc.) are not good candidates: their
import mechanics differ substantially and the annotation-oriented extensions largely do not apply.
C# (attributes) could be a separate, independently designed candidate.
These extensions are deferred until the kotlin-spring implementation is practically proven and stable in at least 1 version.
PR checklist
- Read the contribution guidelines.
- Run the following to build the project and update samples:
(For Windows users, please run the script in WSL)./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example./bin/generate-samples.sh bin/configs/java*.
IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed. - If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
Summary by cubic
Adds the x-extra-imports vendor extension to the kotlin-spring generator so you can declare imports for custom annotations and code, enabling short annotation names in generated files. Imports are file-scoped, deduplicated, and only rendered where needed.
-
New Features
x-extra-importson models, fields, operations, and parameters; accepts a string or list.- Values are emitted verbatim (supports alias imports like
Foo as Bar); exact duplicates removed per file and deduped against generated type imports. - Correct scoping with both tag-based and path-segment grouping (
useTagson/off); multi-tag operations add imports to each API file. - Operation imports rendered only in
api.mustacheandapiInterface.mustacheusingoperationExtraImports(no leakage to controller/delegate wrappers). - Parameter imports include implicit header params when
implicitHeadersis enabled. - Purely additive; works alongside
x-class-extra-annotation,x-field-extra-annotation, andx-operation-extra-annotation.
-
Dependencies
- Updated petstore sample
resttemplate-springBoot4-jackson3tospring-web7.0.5.
- Updated petstore sample