[JAVA-SPRING;KOTLIN-SPRING] feature: x-field-extra-annotation parity for kotlin-spring params + new x-request-body-extra-annotation + --inject-operation-vendor-extensions - #24780
Open
Picazsoo wants to merge 20 commits into
Open
Conversation
…ion-level annotations into body parameters
…d parameter vendor extensions Injects vendor extensions onto operations and their parameters from the CLI or config without editing the spec, complementing --inject-model-vendor-extensions. Applied in DefaultCodegen.fromOperation so it works for all generators and flows through Spring's request-body annotation normalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
x-field-extra-annotation parity for kotlin-spring params + new x-request-body-extra-annotation + --inject-operation-vendor-extensions
…ation placements Adds regression coverage proving x-field-extra-annotation declared on the inline requestBody object and on a reusable components.requestBodies object renders on the generated body parameter in java-spring and kotlin-spring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picazsoo
marked this pull request as ready for review
August 26, 2026 11:31
Picazsoo
marked this pull request as draft
August 26, 2026 11:31
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 27 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- GeneratorSettings: include injectModelVendorExtensions and injectOperationVendorExtensions in equals() and hashCode() so configs differing only in these maps are no longer treated as equal. - JavaCamelServerCodegen: stop advertising x-request-body-extra-annotation, which its Camel REST DSL templates never render; regenerate java-camel docs. - DefaultCodegen: move the shared parameter vendor-extension normalization (normalizeOperationParameterVendorExtensions) up from AbstractJavaCodegen and reuse it from KotlinSpringServerCodegen, removing the duplicate helpers. - DefaultCodegen.injectOperationVendorExtensions: match the spec-authored operationId (operationIdOriginal) when present, falling back to the generated operationId only when the spec omits one. - Correct docs/help to describe the parameter key segment as the spec name (paramBaseName / baseName) to match the actual matching logic. - Tests: add snake_case operationId injection test and a JavaCamel supported vendor-extension test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 29 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- DefaultCodegen.injectOperationVendorExtensions: treat a blank operationIdOriginal like a missing one and fall back to the generated operationId, so injection is not silently skipped when the spec declares an empty operationId. - DefaultCodegenTest: use expected-first argument order for JUnit assertEquals and add a regression test covering the blank-operationId fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
op.operationId is always non-blank at injection time because getOrGenerateOperationId synthesizes one from the path and HTTP method when the spec omits or blanks it. Replace the unreachable isBlank(matchOperationId) early-return with an explicit Objects.requireNonNull on op.operationId so the invariant is documented and a future regression fails loudly instead of silently dropping injected extensions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mples Add compile coverage for the Spring extra-annotation features by copying the shared petstore specs, adding the extension annotations to the copies, and repointing four existing (already-compiled) samples to them. The originals are left untouched, so there is no ripple to the 140+ other configs and no new build target. Copied specs (originals unchanged): - 3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml - 3_0/kotlin/petstore-with-extra-annotation.yaml Repointed samples (cover java/kotlin x reactive/non-reactive): - springboot-useoptional (java, useOptional body branch) - springboot-reactive (java, Mono/Flux body branch) - kotlin-springboot-delegate (kotlin, non-reactive) - kotlin-springboot-reactive (kotlin, Flow/suspend body branch) Exercised, all verified to compile locally (mvn + gradle): - operation-level x-request-body-extra-annotation on addPet (body is a $ref), with updatePet left un-annotated to prove per-operation selectivity - param x-field-extra-annotation on path (getPetById), list-valued query (findPetsByStatus, two annotations), and form (uploadFile) params - kotlin references short names imported via x-extra-imports; java uses fully-qualified Spring @nonnull (java-spring has no x-extra-imports support) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The x-field-extra-annotation section in JavaSpring/bodyParams.mustache emitted
the annotation with a trailing space and no leading space. Because it sits
directly after {{>paramDoc}} (which ends in ")" with no trailing space), the
result glued the annotation to the @parameter(...) close paren and produced a
double space before @Valid, e.g.
...required = true)@org.springframework.lang.NonNull @Valid @RequestBody
Switch to a leading-space style (matching the surrounding binding annotations)
so the output is now:
...required = true) @org.springframework.lang.NonNull @Valid @RequestBody
Only java-spring was affected; the kotlin-spring @parameter block already ends
with a trailing space, so its output was already correct. The section renders
nothing when the extension is absent, so no other samples change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mples Extend the four repointed samples so they also cover the side-loading path: an injectOperationVendorExtensions: block in each sample config (the config-file equivalent of the --inject-operation-vendor-extensions CLI flag) injects the extensions without editing the spec. Injected onto store operations (kept separate from the pet operations used for the spec-declared demo): - placeOrder: operation-level x-request-body-extra-annotation - getOrderById: parameter-level x-field-extra-annotation on the path param The java base spec names that path param order_id while the kotlin base spec names it orderId, so the two configs use different keys. This validates that the parameter segment is matched against the raw spec paramBaseName. Values use the fully-qualified @org.springframework.lang.NonNull, so the injected demo needs no imports and compiles on its own. Regenerated StoreApi for all four samples (java + kotlin, reactive + non-reactive); the injected annotations render before the placeOrder body binding (incl. Mono<Order> in the java reactive sample) and before the getOrderById path param. All four samples compile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…otations The kotlin generator collects x-extra-imports from operation and parameter vendor extensions, and those extensions can themselves be side-loaded. Inject x-extra-imports next to the injected annotations on the two kotlin samples so the injected annotation can use the short name instead of a fully-qualified one: placeOrder.x-request-body-extra-annotation: "@nonnull" placeOrder.x-extra-imports: org.springframework.lang.NonNull getOrderById.orderId.x-field-extra-annotation: "@nonnull" getOrderById.orderId.x-extra-imports: org.springframework.lang.NonNull Regenerated StoreApi for both kotlin samples: the injected import is added to the file and the short @nonnull renders on both the placeOrder body and the getOrderById path param. Both samples compile. The java samples keep the fully-qualified form, since java-spring has no x-extra-imports support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bring CLI/plugin parity for the vendor-extension side-loading feature by exposing injectModelVendorExtensions and injectOperationVendorExtensions on both the Gradle and Maven plugins (previously only reachable via a configFile). - Gradle plugin: new mapProperty extension fields, plugin wiring, and the four GenerateTask mirror points (WorkParameters, execute, task inputs, parameters). - Maven plugin: two List<String> KVP @parameter fields with guarded applyInject*KvpList calls. - Docs: Gradle README.adoc and Maven README.md config tables; CLI help now clarifies that multiple annotations in a single value are space-separated, since an unquoted comma separates different injection targets. - Tests: Gradle ParameterWiringRegressionTest wiring test and Maven CodeGenMojoTest inject-vendor-extensions resource project asserting the injected request-body annotation renders on the generated Spring API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify in the Gradle plugin README that multiple annotations in a single injected value are space-separated (emitted verbatim, safe inside parentheses), with a groovy example. Note that commas inside a value need no escaping in the Gradle map form, unlike the comma-separated CLI/Maven KVP form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…anism Reword the Gradle/Maven/CLI docs to describe injectModelVendorExtensions and injectOperationVendorExtensions as a generic vendor-extension mechanism: values are strings, applied at render time, and overwrite existing values; missing targets are a silent no-op. The space-vs-comma guidance is scoped to the extra-annotation extensions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picazsoo
marked this pull request as ready for review
August 26, 2026 15:42
Picazsoo
marked this pull request as draft
August 26, 2026 15:42
Contributor
Author
|
@cubic-dev-ai, please re-review |
Contributor
@Picazsoo I have started the AI code review. It will take a few minutes to complete. |
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 56 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Strengthen testInjectOperationVendorExtensions so it no longer passes on a mere substring match anywhere in the generated sources. It now locates PetApi.java, asserts the injected @com.example.MyValidation sits on addPet's @RequestBody body parameter, asserts a control operation (updatePet, which also has a body but no injection) does not receive it, and asserts the annotation appears exactly once. This guards the operation-scoped merge against non-selective or wrong-target regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rams The inject-model/operation-vendor-extensions settings are top-level configurator options, not per-generator CliOptions, so a key placed in <configOptions> is never forwarded (CodeGenMojo only forwards keys matching config.cliOptions(), plus SOURCE_FOLDER). The guard therefore protected against an unreachable double-application. Simplify to a plain null check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
name-mappings, parameter-name-mappings, model-name-mappings, enum-name-mappings and operation-id-name-mappings are not generator CliOptions and have no configOptions backwards-compat reader, so their configOptions.containsKey(...) guards protected against an unreachable double-application. Simplify to plain null checks. The inline-schema-options guard is retained because it does have a compat reader. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picazsoo
marked this pull request as ready for review
August 26, 2026 23:50
Picazsoo
marked this pull request as draft
August 26, 2026 23:50
Picazsoo
marked this pull request as ready for review
August 26, 2026 23:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds custom-annotation support for previously uncovered spots in the Spring generators:
x-field-extra-annotationon kotlin-spring path/query/form/header params: brings kotlin-springto parity with java-spring, which already renders this extension on parameters.
x-request-body-extra-annotation(java-spring and kotlin-spring):lets you attach annotations to the generated request-body parameter, even when the body
$refs ashared model.
x-field-extra-annotationon cookie params (java-spring and kotlin-spring): closes thelast parameter kind that supported it in neither generator.
--inject-operation-vendor-extensionsCLI/config flag (all generators): injects vendorextensions onto operations and their parameters from the command line or config, without editing
the spec. This complements the existing
--inject-model-vendor-extensionsflag and is the naturalway to apply the two extensions above when you cannot or do not want to touch the source contract.
Both accept a string or a list of strings, are applied per operation (selective), and are a
no-op when absent.
Motivation
Users commonly reference reusable ID/model schemas from many operations:
To add a validation/framework annotation to a specific usage (e.g. Hibernate Validator / LSP rules
that require constraints to live on the generated interface, not the impl), you need a placement that
is both ref-safe and per-operation:
Path / query / form / header / cookie params support two placements, and you can pick based
on the scope you want:
OrgId): applies globally to every parameter that$refs that schema. Use this when the annotation should always accompany the type. The generatoralready merges a referenced parameter schema's extensions onto the parameter, so this works for
the simple alias schemas typically used by parameters.
name/in): applies selectively to that singleusage, and keeps the shared
schema: { $ref: ... }clean and reusable for other operations. Usethis when only some usages should carry the annotation.
while kotlin-spring rendered it on none of them. Parts 1 and 3 close those gaps so every parameter
kind behaves consistently in both generators.
Request bodies usually
$refa shared model, so there is no per-usage object to annotate:$refare ignored (OpenAPI 3.0/3.1),Part 2 solves this with an operation-level extension: operations are never
$reftargets, sothe annotation is inherently per-usage and ref-safe.
Example (Part 1: parameter object)
Example (Part 2: request body)
Generates (java-spring):
Request-body annotation placements (three working scopes)
Besides the new operation-level extension, the body param also honors annotations declared on the
RequestBody Object itself (these are merged onto the generated body parameter). This gives three
placements, chosen by the scope you want:
x-request-body-extra-annotation(the new extension): per operation. Workseven when the operation's
requestBodyis a bare$ref.requestBodyobjectx-field-extra-annotation: per operation.components.requestBodiesobjectx-field-extra-annotation: applies to everyoperation that
$refs that reusable request body (a shared subset).Caveat: a key placed as a sibling of
$refin the operation'srequestBodyis ignored (OpenAPI3.0/3.1), so to annotate a
$ref-ed reusable body the extension must live inside the reusablecomponents.requestBodiesobject (option 3), not next to the$ref.Example (injecting the extensions from the CLI, no spec edit)
When you cannot modify the source contract, the same result is achievable via
--inject-operation-vendor-extensions:Key formats:
operationId.x-extension-name=valuetargets the operation (for example the request-bodyextension above).
operationId.paramBaseName.x-extension-name=valuetargets a parameter, matched by its specname (
baseName). TheoperationIdsegment is matched against the spec-authored operationId whenpresent, falling back to the generated operationId only when the spec omits one.
Changes
New extension & wiring
VendorExtension: addedX_REQUEST_BODY_EXTRA_ANNOTATION(OPERATION level).getSupportedVendorExtensions()for bothspringandkotlin-spring.DefaultCodegen: new shared helpermergeOperationVendorExtensionIntoBodyParams(...)that copiesthe operation-level values onto the body parameter's
x-field-extra-annotationlist, so bothextensions render through the existing body-param template path.
List<String>) applied in bothSpringCodegenandKotlinSpringServerCodegen(the latter gains a small parameter-normalization helper, since it doesnot share the Java generator's base class).
Vendor-extension injection flag (all generators)
--inject-operation-vendor-extensionsCLI option (repeatable) plus the matchingCodegenConfigurator.addInjectOperationVendorExtension/GeneratorSettingswiring, mirroring theexisting
--inject-model-vendor-extensionsplumbing.DefaultCodegen.fromOperation, so it runs for every generator and beforegenerator-specific post-processing. This means an injected
x-request-body-extra-annotationflowsthrough the same normalize-and-merge path as one declared in the spec.
Templates
pathParams/queryParams/formParams/headerParams: renderx-field-extra-annotationbefore the parameter binding (java-spring parity).cookieParams: renderx-field-extra-annotationbefore@CookieValue/ the cookie binding (previously unsupported in both).
bodyParams: render the merged annotation before@RequestBody/the body binding. Covers
useOptionaland reactive (Mono/Flux,Flow/suspend) variants.parameter collections to
List<String>, so only the templates were missing.Samples (compile-verified)
shared petstore specs were copied and the copies annotated:
petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml(java) and3_0/kotlin/petstore-with-extra-annotation.yaml(kotlin). The originals are untouched, so there isno ripple to the 140+ other configs that consume them.
non-reactive:
springboot-useoptional(java,useOptional),springboot-reactive(java,Mono/Flux),kotlin-springboot-delegate(kotlin), andkotlin-springboot-reactive(kotlin,Flow/suspend). All four compile locally.x-request-body-extra-annotationonaddPet(a
$refbody) withupdatePetleft un-annotated (selectivity); paramx-field-extra-annotationon a path, a list-valued query, and a form param; and, on the kotlin copy,
x-extra-importsso theannotations are referenced by short name.
--inject-operation-vendor-extensionsside-loading path(expressed as an
injectOperationVendorExtensions:block in each sample config, the config-fileequivalent of the CLI flag). Without editing the spec, they inject an operation-level
x-request-body-extra-annotationontoplaceOrderand a parameter-levelx-field-extra-annotationonto the
getOrderByIdpath param. Because the java base spec names that paramorder_idwhile thekotlin base spec names it
orderId, the two configs use different keys, which validates that theparameter segment is matched against the raw spec
paramBaseName. On the kotlin samples the configsalso side-load
x-extra-importsnext to the injected annotations, so the injected@NonNullisimported and referenced by short name (the java samples keep the fully-qualified form, since
java-spring has no
x-extra-importssupport). The injected annotations render and compile alongsidethe spec-declared ones.
Docs
docs/generators/spring.mdanddocs/generators/kotlin-spring.md(vendor-extensiontables) via the docs task, not hand-edited. (No table change for the header/cookie additions, which
reuse the existing
x-field-extra-annotation.)Tests
$refbody + a second operation on the same modelwithout the extension (proves selectivity); covers a non-default body branch. Cookie-param
coverage added to the existing parameter-annotation test.
x-field-extra-annotationon path/query/form/header/cookie params (incl.list-valued cases), plus request-body annotation with selectivity.
DefaultCodegenTest(operation-level and parameter-level landing,non-matching operationId is a no-op) plus java-spring and kotlin-spring end-to-end tests that inject
both extensions and assert selective rendering.
x-field-extra-annotationdeclared on the inlinerequestBodyobject and on a reusablecomponents.requestBodiesobject (referenced by two operations) render on the body param.Design notes / trade-offs
x-field-extra-annotationinstead of a new template branch. The operation-levelrequest-body value is merged onto the body parameter's existing annotation list, so rendering
reuses one code path and multiple annotations compose naturally. Existing param-level annotations
are preserved and appear first.
List<String>. A single string and a list are handled uniformly, extravalues can be appended, and an absent extension yields an empty list → renders nothing (no change
when unused).
the shared schema applies it globally to every usage of that schema; placing it on the Parameter
Object applies it selectively to a single usage and keeps the shared
$refreusable. Both arevalid; choose based on whether you want global or per-usage scope.
Known limitation (out of scope)
If a
requestBodydeclares different schemas per content type, the generator still models it as asingle body parameter / single
@RequestBodyargument (Spring itself binds one body per method),so the annotation applies to that one binding, and it cannot be varied per content type. Generating one
method per content type (via
consumesdispatch) would be the fully spec-faithful approach but is abroader change (it breaks the 1:1 operation→method contract across all generators) and is not
addressed here. The operation-level design does not preclude adding a media-type-level extension
later if that path is ever taken.
Acceptance
x-field-extra-annotationon path/query/form/header params (java-springparity), including list values; model-field behavior unchanged.
x-field-extra-annotationrenders on cookie params in both java-spring and kotlin-spring.x-request-body-extra-annotationrenders before the body param in java-spring and kotlin-spring,works with
$refbodies, is selective per operation, and coversuseOptional/ reactive variants.--inject-operation-vendor-extensionsapplies operation- and parameter-level extensions from theCLI/config for any generator, selectively per operation, with an empty/absent map being a no-op.
of the shared petstore specs and compile with the emitted annotations, verifying the feature
end-to-end without adding new build targets. The same samples also inject both extensions via an
injectOperationVendorExtensions:config block (the side-loading path), confirming the CLI/configinjection mechanism renders and compiles too.
Plugin parity (Gradle + Maven)
The
injectModelVendorExtensions/injectOperationVendorExtensionsside-loading maps were onlyreachable from the plugins through a
configFile. They are now first-class properties on both theGradle and Maven plugins, matching the CLI and the existing parity of the other mapping properties
(
nameMappings,globalProperties, etc.):openApiGenerate { ... }): twoMapProperty<String, String>fields wired through theextension, plugin, and
GenerateTaskto the configurator.<configuration>): twoList<String>KVP parameters(
openapi.generator.maven.plugin.inject{Model,Operation}VendorExtensions) applied via the sharedapplyInject*KvpListhelpers. These settings are configurator-level (not generatorCliOptions)and have no
configOptionsbackwards-compat reader, so unlike the legacy mapping options they needno
configOptionsguard; the same review also removed the equally deadconfigOptionsguards fromthe five
*-name-mappingsparameters (theinline-schema-optionsguard is kept, since it does havea compat reader).
README.adocand MavenREADME.mdconfig tables. The CLI help for--inject-*-vendor-extensionsnow clarifies that multiple annotations in a single value arespace-separated (emitted verbatim into source); an unquoted comma is the separator between
different injection targets, so comma-bearing annotation attributes should use the spec-level
extension (string or YAML list) instead.
ParameterWiringRegressionTestcase asserts the injected annotation reachesgenerated sources, and a Maven
CodeGenMojoTestcase (newinject-vendor-extensionsresourceproject) asserts the injected
x-request-body-extra-annotationis merged onto the body parameterand rendered on the generated Spring API.
PR checklist
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.