Skip to content

[Spring-http-interface] JSpecify: fix api argument with default value - #24740

Open
jpfinne wants to merge 38 commits into
OpenAPITools:masterfrom
jpfinne:bugfix_24737
Open

jpfinne wants to merge 38 commits into
OpenAPITools:masterfrom
jpfinne:bugfix_24737

Conversation

@jpfinne

@jpfinne jpfinne commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

fix #24737

For spring-http-interface only: the api arguments with default value are annotated with @Nullable.
Spring will inject the specified default value if null is provided.

Small refactoring of the spring templates:
nullableAnnotation.mustache is kept for model only
nullableAnnotation_apiArgument.mustache is introduced for all api arguments (queryParams, pathParams, headerParams)

This make it easier to redefine nullableAnnotation_apiArgument for spring-http-interface to handle the default parameters.

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.

@cachescrubber (2022/02) @welshm (2022/02) @MelleD (2022/02) @atextor (2022/02) @manedev79 (2022/02) @javisst (2022/02) @borsch (2022/02) @banlevente (2022/02) @Zomzog (2022/09) @martin-mfg (2023/08) @KannaKim (2026/07)


Summary by cubic

Fixes spring-http-interface JSpecify handling so API arguments with default values or required=false nullable schemas are marked @Nullable, matching Spring's actual injection behavior.

  • Introduces nullableAnnotation_apiArgument.mustache plus a spring-http-interface override, and switches body, query, header, cookie, and path templates to use it; nullableAnnotation.mustache stays for models only.
  • Simplifies nullableAnnotation_jspecify.mustache to remove the redundant guard, preventing duplicate @Nullable.
  • Updates the JSpecify test spec to cover defaulted and nullable query/header parameters and regenerates samples.

Migration

  • If you enable JSpecify, ensure org.jspecify:jspecify is on the classpath.

Written for commit 6432c0b. Summary will update on new commits.

Review in cubic

@anthonyvdotbe

Copy link
Copy Markdown
Contributor

@jpfinne is this ready for review? If not, what remains to be done? I'd like to get this fixed ASAP, so if needed I can create a PR myself

@jpfinne jpfinne changed the title Fix 24737 [Spring-http-interface] Jspecify: fix api argument with default value Aug 20, 2026
@jpfinne jpfinne changed the title [Spring-http-interface] Jspecify: fix api argument with default value [Spring-http-interface] JSpecify: fix api argument with default value Aug 20, 2026
@jpfinne
jpfinne marked this pull request as ready for review August 20, 2026 17:21
@jpfinne

jpfinne commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor Author

@jpfinne is this ready for review? If not, what remains to be done? I'd like to get this fixed ASAP, so if needed I can create a PR myself

@anthonyvdotbe I've simplified the nullable annotations to easily distinguish between model and api.
Can you validate and approve the PR?

We are close to the release date... Contact wing328 to accelerate the merge

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 17 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@anthonyvdotbe

Copy link
Copy Markdown
Contributor

@anthonyvdotbe I've simplified the nullable annotations to easily distinguish between model and api. Can you validate and approve the PR?

@jpfinne I see the AI bot suggested changes, so I'll let you resolve those first

@jpfinne

jpfinne commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@anthonyvdotbe I've simplified the nullable annotations to easily distinguish between model and api. Can you validate and approve the PR?

@jpfinne I see the AI bot suggested changes, so I'll let you resolve those first

@anthonyvdotbe done

@anthonyvdotbe anthonyvdotbe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I'd prefer a more focused PR that just fixes this particular issue and leaves the refactoring for another PR.

@KannaKim KannaKim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed a possible edge case in nullableAnnotation_apiArgument.mustache.

For an optional parameter explicitly declared as nullable:

parameters:
  - name: filter
    in: query
    required: false
    schema:
      type: string
      nullable: true

generating a Spring HTTP Interface with openApiNullable=true produces:
@RequestParam(value = "filter", required = false) String filter
instead of:
@RequestParam(value = "filter", required = false) @Nullable String filter
This appears to come from:
{{#openApiNullable}}{{^isNullable}}@Nullable {{/isNullable}}{{/openApiNullable}}

WhenisNullable=true, the inverted section suppresses the annotation. Interestingly, setting openApiNullable=false does generate @Nullable.

It may be worth adding a test covering isNullable=true with openApiNullable=true.

@jpfinne
jpfinne requested a review from KannaKim September 1, 2026 04:39

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 26 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/api/RequiredAndNullableApi.java">

<violation number="1" location="samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/api/RequiredAndNullableApi.java:65">
P1: The generated GET endpoint's `filter` query param is optional (required=false) and nullable per the spec, but is emitted as `String filter` with no `@Nullable`. In the new base `nullableAnnotation_apiArgument.mustache`, when `openApiNullable=true` the annotation is gated on `{{^isNullable}}`, so a param that is explicitly nullable gets no `@Nullable` at all. This contradicts the PR's aim (nullable API arguments should be annotated so Spring handles null) and leaves the newly imported `org.jspecify.annotations.Nullable` unused. Emit `@Nullable` for nullable params too (e.g. also when `isNullable`, or drop the `{{^isNullable}}` guard and gate only on `{{^required}}`/`{{^useOptional}}`).</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

value = RequiredAndNullableApi.PATH_REQUIRED_AND_NULLABLE_GET
)
default ResponseEntity<Void> requiredAndNullableGet(
@Parameter(name = "filter", description = "", in = ParameterIn.QUERY) @Valid @RequestParam(value = "filter", required = false) String filter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The generated GET endpoint's filter query param is optional (required=false) and nullable per the spec, but is emitted as String filter with no @Nullable. In the new base nullableAnnotation_apiArgument.mustache, when openApiNullable=true the annotation is gated on {{^isNullable}}, so a param that is explicitly nullable gets no @Nullable at all. This contradicts the PR's aim (nullable API arguments should be annotated so Spring handles null) and leaves the newly imported org.jspecify.annotations.Nullable unused. Emit @Nullable for nullable params too (e.g. also when isNullable, or drop the {{^isNullable}} guard and gate only on {{^required}}/{{^useOptional}}).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/api/RequiredAndNullableApi.java, line 65:

<comment>The generated GET endpoint's `filter` query param is optional (required=false) and nullable per the spec, but is emitted as `String filter` with no `@Nullable`. In the new base `nullableAnnotation_apiArgument.mustache`, when `openApiNullable=true` the annotation is gated on `{{^isNullable}}`, so a param that is explicitly nullable gets no `@Nullable` at all. This contradicts the PR's aim (nullable API arguments should be annotated so Spring handles null) and leaves the newly imported `org.jspecify.annotations.Nullable` unused. Emit `@Nullable` for nullable params too (e.g. also when `isNullable`, or drop the `{{^isNullable}}` guard and gate only on `{{^required}}`/`{{^useOptional}}`).</comment>

<file context>
@@ -42,6 +43,32 @@ default Optional<NativeWebRequest> getRequest() {
+        value = RequiredAndNullableApi.PATH_REQUIRED_AND_NULLABLE_GET
+    )
+    default ResponseEntity<Void> requiredAndNullableGet(
+        @Parameter(name = "filter", description = "", in = ParameterIn.QUERY) @Valid @RequestParam(value = "filter", required = false) String filter
+    ) {
+        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 40 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml">

<violation number="1" location="modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml:53">
P3: Operations are now tagged `nble`, but the top-level `tags:` list still declares `requiredAndNullable` (now unused) and has no `nble` entry. Update the top-level tag list to match the renamed operations so the fixture does not carry a stale tag declaration.</violation>
</file>

<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java">

<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java:7239">
P3: In testJspecify the nbleApi.fileContains(") String mandatory", ") @Nullable String optional") block is now byte-for-byte identical in the SPRING_HTTP_INTERFACE branch and the else branch, so the if/else reads as if it varies the NbleApi assertions when only the fooApiAssert color value differs. Hoist the identical nbleApi assertion above the if/else and keep only the differing color assertion inside each branch.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

/nble:
get:
tags:
- nble

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Operations are now tagged nble, but the top-level tags: list still declares requiredAndNullable (now unused) and has no nble entry. Update the top-level tag list to match the renamed operations so the fixture does not carry a stale tag declaration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml, line 53:

<comment>Operations are now tagged `nble`, but the top-level `tags:` list still declares `requiredAndNullable` (now unused) and has no `nble` entry. Update the top-level tag list to match the renamed operations so the fixture does not carry a stale tag declaration.</comment>

<file context>
@@ -47,23 +47,29 @@ paths:
     get:
       tags:
-        - requiredAndNullable
+        - nble
       parameters:
-        - name: filter
</file context>

if (SPRING_HTTP_INTERFACE.equals(library)) {
nbleApi.fileContains(
") String mandatory",
") @Nullable String optional");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: In testJspecify the nbleApi.fileContains(") String mandatory", ") @nullable String optional") block is now byte-for-byte identical in the SPRING_HTTP_INTERFACE branch and the else branch, so the if/else reads as if it varies the NbleApi assertions when only the fooApiAssert color value differs. Hoist the identical nbleApi assertion above the if/else and keep only the differing color assertion inside each branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java, line 7239:

<comment>In testJspecify the nbleApi.fileContains(") String mandatory", ") @Nullable String optional") block is now byte-for-byte identical in the SPRING_HTTP_INTERFACE branch and the else branch, so the if/else reads as if it varies the NbleApi assertions when only the fooApiAssert color value differs. Hoist the identical nbleApi assertion above the if/else and keep only the differing color assertion inside each branch.</comment>

<file context>
@@ -7231,16 +7231,18 @@ public void testJspecify(String library, int springBootVersion) throws IOExcepti
-                    " @Nullable String filter");
+            nbleApi.fileContains(
+                    ") String mandatory",
+                    ") @Nullable String optional");
             fooApiAssert.fileContains(
                         "@RequestParam(value = \"color\", required = false, defaultValue = \"red\") @Nullable String color");
</file context>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][Spring] Wrong nullability of method parameters with spring-http-interface library

3 participants