Skip to content

[Java][RestClient] Mark non-required API method parameters @Nullable under JSpecify#24392

Open
seonwooj0810 wants to merge 1 commit into
OpenAPITools:masterfrom
seonwooj0810:fix/issue-24388-restclient-jspecify-nullable-params
Open

[Java][RestClient] Mark non-required API method parameters @Nullable under JSpecify#24392
seonwooj0810 wants to merge 1 commit into
OpenAPITools:masterfrom
seonwooj0810:fix/issue-24388-restclient-jspecify-nullable-params

Conversation

@seonwooj0810

@seonwooj0810 seonwooj0810 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #24388

Root cause

Under useJspecify, the Java restclient library rendered the public {operationId} and {operationId}WithHttpInfo method parameters via {{>nullable_var_annotations}} {{{dataType}}}. That emits @Nullable as a prefix, which JSpecifyNullableLambda then strips (so it can be re-injected TYPE_USE-style into the datatype by the jSpecifyDatatype lambda). Because these two methods did not wrap the datatype in {{#lambda.jSpecifyDatatype}}, the stripped @Nullable was never re-injected and the annotation was dropped entirely — so non-required parameters were left unmarked even though the generated package is @NullMarked. The private *RequestCreation and public *WithResponseSpec methods already used {{>nullableArgument}} and rendered correctly, which is why the same parameters were @Nullable on those methods but not on {operationId}/{operationId}WithHttpInfo.

Change

api.mustache (restclient library): the two affected public methods now use {{>nullableArgument}} — the same partial the correct-behaving methods already use. nullableArgument = {{>nullable_var_annotations}} {{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}, so the @Nullable is correctly placed inside the type (java.time.@Nullable Instant). In non-JSpecify mode the two forms are equivalent, so output is unchanged there.

Verification done

  • Built the CLI (JDK21) from a freshly-synced master and regenerated the affected sample (java-restclient-springBoot4-jackson3-jspecify). The only diff is the two public methods now marking the non-required dtParam/dtQuery/dtCookie as java.time.@Nullable Instant, byte-for-byte identical to the already-correct private fooDtParamGetRequestCreation in the same file (which uses the existing org.jspecify.annotations.Nullable import). A required parameter (fileIdGet(String id)) correctly stays non-null.
  • Regenerated the non-JSpecify restclient samples (java-restclient, java-restclient-springBoot4-jackson3, java-restclient-springBoot4-jackson2): zero diff, confirming the change is behavior-preserving outside JSpecify.

Summary by cubic

Marks non-required parameters in generated Java RestClient public methods as @Nullable under JSpecify. Fixes #24388 and keeps non-JSpecify output unchanged.

  • Bug Fixes
    • Use {{>nullableArgument}} for params in {operationId} and {operationId}WithHttpInfo to enable type-use placement via jSpecifyDatatype.
    • Non-required params now render as java.time.@Nullable Instant in @NullMarked packages, matching *RequestCreation/*WithResponseSpec.
    • Regenerated samples: JSpecify variant updated as expected; non-JSpecify variants unchanged.

Written for commit 8094378. Summary will update on new commits.

Review in cubic

…nder JSpecify

The public `{operationId}` and `{operationId}WithHttpInfo` methods in the
restclient library rendered parameters via `{{>nullable_var_annotations}}
{{{dataType}}}`, which bypasses the `jSpecifyDatatype` lambda. Under
useJspecify the JSpecifyNullableLambda strips the `@Nullable` prefix so it can
be re-injected TYPE_USE-style into the datatype, but without that lambda the
annotation was dropped entirely, leaving non-required parameters unmarked even
though the generated package is @NullMarked.

Use `{{>nullableArgument}}` (as the private *RequestCreation and
*WithResponseSpec methods already do) so non-required params are consistently
annotated. Behavior is unchanged in non-JSpecify mode.

Fixes OpenAPITools#24388

@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 2 files

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/main/resources/Java/libraries/restclient/api.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/Java/libraries/restclient/api.mustache:183">
P2: Optional generic parameters are annotated at the wrong type-use level after this change: `jSpecifyDatatype` uses the last dot in the whole datatype, so `List<java.time.Instant>` becomes `List<java.time.@Nullable Instant>` instead of annotating the outer `List`. Preserving `nullableArgument` here requires fixing the datatype lambda to place `@Nullable` on the outer parameter type, including nested generic types.</violation>
</file>

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

Re-trigger cubic

@Deprecated
{{/isDeprecated}}
public {{#returnType}}{{#isResponseFile}}{{#useAbstractionForFiles}}org.springframework.core.io.Resource{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{returnType}}}{{/useAbstractionForFiles}}{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<org.springframework.core.io.Resource>{{/collectionFormat}}{{^collectionFormat}}org.springframework.core.io.Resource{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{>nullableArgument}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{>nullable_var_annotations}} {{{dataType}}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws RestClientResponseException {
public {{#returnType}}{{#isResponseFile}}{{#useAbstractionForFiles}}org.springframework.core.io.Resource{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{returnType}}}{{/useAbstractionForFiles}}{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<org.springframework.core.io.Resource>{{/collectionFormat}}{{^collectionFormat}}org.springframework.core.io.Resource{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{>nullableArgument}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{>nullableArgument}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws RestClientResponseException {

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.

P2: Optional generic parameters are annotated at the wrong type-use level after this change: jSpecifyDatatype uses the last dot in the whole datatype, so List<java.time.Instant> becomes List<java.time.@Nullable Instant> instead of annotating the outer List. Preserving nullableArgument here requires fixing the datatype lambda to place @Nullable on the outer parameter type, including nested generic types.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/Java/libraries/restclient/api.mustache, line 183:

<comment>Optional generic parameters are annotated at the wrong type-use level after this change: `jSpecifyDatatype` uses the last dot in the whole datatype, so `List<java.time.Instant>` becomes `List<java.time.@Nullable Instant>` instead of annotating the outer `List`. Preserving `nullableArgument` here requires fixing the datatype lambda to place `@Nullable` on the outer parameter type, including nested generic types.</comment>

<file context>
@@ -180,7 +180,7 @@ public class {{classname}} {
     @Deprecated
     {{/isDeprecated}}
-    public {{#returnType}}{{#isResponseFile}}{{#useAbstractionForFiles}}org.springframework.core.io.Resource{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{returnType}}}{{/useAbstractionForFiles}}{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<org.springframework.core.io.Resource>{{/collectionFormat}}{{^collectionFormat}}org.springframework.core.io.Resource{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{>nullableArgument}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{>nullable_var_annotations}} {{{dataType}}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws RestClientResponseException {
+    public {{#returnType}}{{#isResponseFile}}{{#useAbstractionForFiles}}org.springframework.core.io.Resource{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{returnType}}}{{/useAbstractionForFiles}}{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<org.springframework.core.io.Resource>{{/collectionFormat}}{{^collectionFormat}}org.springframework.core.io.Resource{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{>nullableArgument}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{>nullableArgument}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws RestClientResponseException {
         {{#returnType}}ParameterizedTypeReference<{{#isResponseFile}}{{#useAbstractionForFiles}}org.springframework.core.io.Resource{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{returnType}}}{{/useAbstractionForFiles}}{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}> localVarReturnType = new ParameterizedTypeReference<>() {};{{/returnType}}{{^returnType}}ParameterizedTypeReference<Void> localVarReturnType = new ParameterizedTypeReference<>() {};{{/returnType}}
         {{#returnType}}return {{/returnType}}{{operationId}}RequestCreation({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}).body(localVarReturnType);
</file context>

@Chrimle Chrimle 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.

Looks great, thanks for the contribution! ❤️

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] [Java] [RestClient] NullMarked API does not mark parameters as @Nullable

2 participants