Skip to content

[Java][Spring] Emit @Nullable on non-required fields with a default value under jspecify (#24294)#24338

Open
abhaydubey93 wants to merge 2 commits into
OpenAPITools:masterfrom
abhaydubey93:fix/spring-jspecify-nullable-default-24294
Open

[Java][Spring] Emit @Nullable on non-required fields with a default value under jspecify (#24294)#24338
abhaydubey93 wants to merge 2 commits into
OpenAPITools:masterfrom
abhaydubey93:fix/spring-jspecify-nullable-default-24294

Conversation

@abhaydubey93

@abhaydubey93 abhaydubey93 commented Jul 17, 2026

Copy link
Copy Markdown

[Java][Spring] Emit @nullable on non-required fields with a default value under jspecify (#24294)

Under jspecify, a non-required model field carrying a default value was generated without the @Nullable annotation. A non-required field is nullable regardless of its default, because the caller can still explicitly pass null.

The JavaSpring nullableAnnotation partial gated the annotation behind isNullable in its defaultValue branch, suppressing @nullable for the common case (non-required + default + openApiNullable=false). This scopes the fix to useJspecify so defaulted non-required fields are treated the same as non-defaulted ones, while leaving the non-jspecify path byte-for-byte unchanged.

Adds a regression test (testJspecifyNullableWithDefaultValue_issue24294) with a minimal spec. Full SpringCodegenTest suite passes (290 tests, 0 failures).


Summary by cubic

Emit @Nullable for non-required model properties that have a default when useJspecify is enabled, and keep Spring request parameters with defaults non-null. Fixes missing nullability on defaulted properties while keeping parameter rendering and non-jspecify behavior unchanged.

  • Bug Fixes
    • Update JavaSpring/nullableAnnotation.mustache to apply @Nullable only to defaulted, non-required model fields (exclude isQueryParam/isHeaderParam/isCookieParam/isPathParam/isBodyParam/isFormParam; still respect openApiNullable and useOptional).
    • Extend testJspecifyNullableWithDefaultValue_issue24294 with a minimal spec to verify org.jspecify.annotations.Nullable on defaulted model fields (not on required), and to ensure request parameters with defaults are not annotated while those without defaults remain annotated.

Written for commit 2ff022f. Summary will update on new commits.

Review in cubic

…alue under jspecify (OpenAPITools#24294)

Under jspecify, a non-required model field carrying a `default` value was
generated without the `@Nullable` annotation. A non-required field is nullable
regardless of its default, because the caller can still explicitly pass null.

The JavaSpring `nullableAnnotation` partial gated the annotation behind
`isNullable` in its `defaultValue` branch, suppressing @nullable for the common
case (non-required + default + openApiNullable=false). This scopes the fix to
`useJspecify` so defaulted non-required fields are treated the same as
non-defaulted ones, while leaving the non-jspecify path byte-for-byte unchanged.

Adds a regression test (testJspecifyNullableWithDefaultValue_issue24294) with a
minimal spec. Full SpringCodegenTest suite passes (290 tests, 0 failures).

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

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

Re-trigger cubic

@Mattias-Sehlstedt

Copy link
Copy Markdown
Contributor

Note that I have not used the Spring generator myself, so I do not know how it compares to other languages with regard to how it handles nullability.

From an OAS perspective, isn't this new annotation incorrect? Having

force:
  type: boolean
  default: false

in OAS 3.0 where the property is not required explicitly states that we either expect force: true/false in our payload or that force is not present at all? Since force: null does not abide to the schema since force is not marked with OAS 3.0 nullable?

The specification basically states if force is there it has to be a boolean, if it is omitted the server will handle it as if force was sent with the default.

it is a general issue that most generators and languages do not differentiate between force: true and no force at all. And from a server perspective it is generally only meaningful in certain post/put cases where null can be "unset the value" whereas missing is "keep previous value".

… request params

The previous change made every non-required field with a default value @nullable
under jspecify. Because nullableAnnotation.mustache is shared between POJO model
fields and Spring controller parameters, this incorrectly annotated non-required
query/header/cookie/path/body/form parameters that declare a default value.

Those parameters are non-null at runtime: Spring's @RequestParam/@RequestHeader/
@CookieValue supply the configured defaultValue when the request value is absent,
so @nullable is a wrong nullness contract for them.

Restrict the new behavior to model properties (identified by the absence of the
CodegenParameter isXxxParam flags). Parameter rendering is unchanged from before
this fix. Extends the regression test with a non-required query param carrying a
default (must not be @nullable) alongside one without a default (stays @nullable).

Full SpringCodegenTest suite passes (290 tests, 0 failures).
@abhaydubey93

Copy link
Copy Markdown
Author

Fair point on OAS semantics — strictly, non-required without nullable: true means "valid boolean or absent," and absent ≠ null. Agreed that most generators collapse that distinction.

Two reasons I'd keep the annotation here:

  1. This mapping already exists — the PR only fixes an inconsistency. The Spring generator annotates every non-required property @Nullable, regardless of OAS nullable. E.g. the committed springboot-4-jspecify/Foo.java (no property is nullable: true; only requiredDt is required):

    private @Nullable OffsetDateTime dt;   // not required -> @Nullable
    private @Nullable BigDecimal number;   // not required -> @Nullable
    private OffsetDateTime requiredDt;     // required -> not @Nullable

    Non-required + default was the one case that silently dropped @Nullable — that inconsistency is what [BUG][JAVA][Spring] with jspecify - non-required field with default should be annotated with @Nullable #24294 reported.

  2. The generated field really can be null. The default is just a field initializer: the setter and all-args constructor accept null with no guard (@NotNull is only emitted for required fields). Under jspecify @NullMarked, leaving it un-annotated would claim non-null — a contract the code doesn't enforce. Request params are different (Spring's defaultValue= binding guarantees non-null), which is why the previous revision excludes them.

The deeper missing-vs-null tri-state question is real, but it's a generator-wide design change (every non-required field, every Java target, breaking) — better as a separate issue, which I'm happy to open.

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.

2 participants