[kotlin-spring] feature: Add `x-extra-imports` vendor extension

Merged
PI
3mo ago
+377-1

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:

LevelWhere to put itGenerated into
ModelOn a schemaThe model file's imports
FieldOn a property schemaThe owning model file's imports
OperationOn an operationThe API file's imports
Operation parameterOn a parameterThe 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 (useTags on/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 Bar are 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 new X_EXTRA_IMPORTS extension with levels
    MODEL, FIELD, OPERATION, OPERATION_PARAMETER.
  • KotlinSpringServerCodegen.java
    • Registered the extension in getSupportedVendorExtensions().
    • Added postProcessModels to collect model- and field-level imports into the model file's
      imports.
    • Extended postProcessOperationsWithModels to aggregate operation- and parameter-level
      imports into operationExtraImports for the API file.
    • Import collection reuses the existing DefaultCodegen.getObjectAsStringList(Object) helper,
      the same mechanism used by the sibling x-*-extra-annotation extensions, so single-string
      and list-of-strings inputs are handled identically.
  • Templates: api.mustache and apiInterface.mustache render operationExtraImports
    after the existing {{#imports}} block.
  • Docs: added the x-extra-imports row to docs/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:

  1. they support the sibling x-*-extra-annotation extensions, and
  2. 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.mustache and
    JavaSpring/api.mustache already use the same {{#imports}} structure, so the logic could be
    implemented once in AbstractJavaCodegen (model + field) and SpringCodegen (operation +
    parameter) and be inherited broadly.
  • kotlin-client, which shares AbstractKotlinCodegen with kotlin-spring and whose
    model.mustache renders the same imports block. The model-level logic added here could be lifted
    into AbstractKotlinCodegen and 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:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    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-imports on 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 (useTags on/off); multi-tag operations add imports to each API file.
    • Operation imports rendered only in api.mustache and apiInterface.mustache using operationExtraImports (no leakage to controller/delegate wrappers).
    • Parameter imports include implicit header params when implicitHeaders is enabled.
    • Purely additive; works alongside x-class-extra-annotation, x-field-extra-annotation, and x-operation-extra-annotation.
  • Dependencies

    • Updated petstore sample resttemplate-springBoot4-jackson3 to spring-web 7.0.5.