GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Add support for JSpecify nullness annotations (#1243) by filiphr · Pull Request #4033 · mapstruct/mapstruct

Conversation

@filiphr

@filiphr filiphr commented Apr 12, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Long overdue, I finally got the time to dig into support for NonNull / Nullable. I've decided to only support JSpecify for now, since it seems the ecosystem has converged on that.


Detect @NonNull, @Nullable, @NullMarked and @NullUnmarked from org.jspecify.annotations to control null check generation in mapping code.

Property-level null checks:

  • Source @NonNull: skip null check (value guaranteed non-null)
  • Target @NonNull: always add null check (must protect non-null target)
  • All other cases: defer to existing NullValueCheckStrategy
  • Safety guards (default values, unboxing, NVPMS) are preserved

Method-level source parameter:

  • @NonNull parameter: skip the method-level null guard

@NullMarked / @NullUnmarked scope:

  • In a @NullMarked class or package, unannotated types are effectively @NonNull
  • @NullUnmarked reverts to unknown nullability within its scope
  • @Nullable on a specific type overrides the scope
  • Scope is resolved by walking the enclosing element chain (method -> class -> outer class -> package) and the result is cached on Type.isNullMarked()

Constructor parameters:

  • Report a compile error when mapping a potentially nullable source to a @nonnull constructor parameter without a defaultValue, since a null check would leave the variable at null violating the contract
    Fixes Support for JSpecify nullness annotations #1243

Would like to get the feedback from the community on this one.


Some open things that could go in followup PRs:

  • Tests with Kotlin nullable / non-null types

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mk868, pkernevez, user99442, vmckinney-cainc, sisimomo, francois-sage, SackCastellon, and is-ivanov reacted with heart emoji
All reactions
  • ❤️ 8 reactions
@filiphr
Add support for JSpecify nullness annotations (mapstruct#1243)
4557780
Detect @nonnull, @nullable, @NullMarked and @NullUnmarked from
org.jspecify.annotations to control null check generation in
mapping code.

Property-level null checks:
- Source @nonnull: skip null check (value guaranteed non-null)
- Target @nonnull: always add null check (must protect non-null target)
- All other cases: defer to existing NullValueCheckStrategy
- Safety guards (default values, unboxing, NVPMS) are preserved

Method-level source parameter:
- @nonnull parameter: skip the method-level null guard

@NullMarked / @NullUnmarked scope:
- In a @NullMarked class or package, unannotated types are effectively
  @nonnull
- @NullUnmarked reverts to unknown nullability within its scope
- @nullable on a specific type overrides the scope
- Scope is resolved by walking the enclosing element chain
  (method -> class -> outer class -> package) and the result is
  cached on Type.isNullMarked()

Constructor parameters:
- Report a compile error when mapping a potentially nullable source to
  a @nonnull constructor parameter without a defaultValue, since a null
  check would leave the variable at null violating the contract
@filiphr filiphr mentioned this pull request Apr 12, 2026
Support for JSpecify nullness annotations #1243
Closed
filiphr added 5 commits April 13, 2026 09:43
@filiphr
Fix JSpecify nullness edge cases and expand test coverage
3f781e5
Address two correctness issues in the JSpecify support added in mapstruct#1243:

- Direct parameter mappings (no property entries) now consult the source
  parameter's own nullability when checking against a @nonnull constructor
  parameter, eliminating a false-positive "potentially nullable source" error
  when a @nonnull method parameter is wired directly into a @nonnull
  constructor argument.
- Target-side @NullMarked scope resolution now uses the bean that declares
  the setter/field instead of the property value type, so unannotated setter
  parameters inside a @NullMarked bean are correctly promoted to @nonnull.

Add tests covering direct-parameter constructor mappings (positive and
erroneous), @NullUnmarked scope override, explicit @nullable overriding an
enclosing @NullMarked scope, a @NullMarked target bean's unannotated setter,
defaultValue suppressing the constructor-parameter error, behavioral
assertion for the skipped method-level guard on a @nonnull parameter, and
preservation of the unboxing, defaultValue, and SET_TO_DEFAULT (update
method) safety guards for @nullable sources.
@filiphr
Harden JSpecify nullness support and surface decisions
3a7880d
Address several follow-ups from the review of mapstruct#1243:

- Honor element-level @NullMarked / @NullUnmarked by walking the element's
  own enclosing chain up to the declaring type before consulting the
  bean-type scope. A @NullUnmarked method inside a @NullMarked class now
  correctly reverts unannotated types back to unknown nullability.
- Guard unchecked (TypeElement) casts in NullabilityUtils and Type against
  annotation elements that are not TypeElements (e.g. ErrorType during
  incremental builds), which would otherwise surface as a processor crash.
- Tighten getSetterNullability: a zero-parameter ExecutableElement is not a
  valid write accessor, so return UNKNOWN instead of silently falling
  through to the return-type path.
- Emit verbose Messager notes at every JSpecify-driven null-check decision
  (setter skip, setter add, method-level guard skip) so the behavior is
  visible under -Amapstruct.verbose=true rather than silent.

Also expand Javadoc on JSpecifyConstants fields, Type.isNullMarked (scope
walk + cache semantics), and NullabilityUtils.getNullability /
getSetterNullability (supplier contract, field + zero-arg handling), and
align the constructor-parameter error comment with its actual condition
(fires on any source not guaranteed @nonnull, not only explicit @nullable).
@filiphr
Make JSpecify test classes package-private
5af89e4
@filiphr
Skip JSpecify @nonnull constructor-param error when assignment failed
2b98223
When the mapping resolver could not find an assignment (e.g. incompatible
source/target types), the JSpecify @nonnull constructor-param check still
fired, producing a misleading duplicate error on top of the primary
"can't find mapping" diagnostic. Guard the check on assignment != null so
users see the root-cause error alone.
@filiphr
Clean up JSpecify note messages and Javadoc accuracy
f485467
Split the single PROPERTYMAPPING_JSPECIFY_NOTE template into dedicated
Message entries so the English phrasing lives in the enum rather than
being passed as format arguments from every call site.

Also correct Javadoc that overstated behavior:
- Type#isNullMarked: module support is conditional on JPMS, and TypeFactory
  does not intern Type instances, so the "O(1)" / "interned" claim was
  misleading. Reword to memoization + caller guidance.
- JSpecifyConstants: make NULL_MARKED_FQN / NULL_UNMARKED_FQN wording
  symmetric about closest-annotation precedence.
- PropertyMapping constructor-param check: clarify that the diagnostic is
  a hard compile error, not a passive note.

Loosen JSpecifyVerboseNoteTest regex to match the nullability enum values
positionally rather than pinning exact constant names.
@nealeu

nealeu commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

This looks great @filiphr. Given the JDK 21 failure, are you going to add a Java 25 (26 too - i.e. last non-LTS) into the CI matrix?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@nealeu nealeu mentioned this pull request Apr 13, 2026
Evaluate @Nullable annotations to support combining mapstruct with uber/nullaway #3182
Closed
@filiphr filiphr mentioned this pull request Apr 13, 2026
Cache Type instances in TypeFactory.getType(...) to avoid redundant work #4034
Open
@filiphr

filiphr commented Apr 13, 2026

Copy link
Copy Markdown
Member Author

Given the JDK 21 failure, are you going to add a Java 25 (26 too - i.e. last non-LTS) into the CI matrix?

It's a silly error in the test code. Will be fixed. I'll add Java 25 and 26 separately into the CI matrix, it's not linked to this issue in particular.

nealeu and gurleen263 reacted with heart emoji
All reactions
  • ❤️ 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@filiphr
Fix JSpecify fixture filename casing for case-sensitive filesystems
176784a
The test passed "withoutpackageinfo" (lowercase) as the variant but the
fixture file on disk is PackageNullMarkedMapperImpl_withoutPackageInfo.java
(PascalCase). macOS's case-insensitive filesystem matched them locally;
Linux CI (case-sensitive) could not find the fixture and failed.
@nealeu

nealeu commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

I don't want to delay you merging what you have because it's already really valuable and will make a big difference for us.

However, I'm not clear if this does or doesn't cover type use on array elements. Some permutations of copies between the arrays below should fail.

@NullMarked
class JSpecifyArrays {

  public String @Nullable[] getNullableArrayOfNonNullStrings() { }

  public @Nullable String[] getNonNullArrayOfNullableStrings() { }

  public String[] getNonNullArrayOfNonNullStrings() { }
}
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@filiphr

filiphr commented Apr 13, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

Thanks for looking into this @nealeu. I think that second 2 should be covered

  public @Nullable String[] getNonNullArrayOfNullableStrings() { }

  public String[] getNonNullArrayOfNonNullStrings() { }

However, I think that the first one would be treated the same as the getNonNullArrayOfNonNullStrings.

I'll create another issue for this, as that looks a bit more involved.

nealeu reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This was referenced Apr 13, 2026
Review JSpecify nullness on type user array elements and lists #4035
Open
Decide whether or not we should mark generated code with JSpecify annotations #4036
Open
@filiphr
Document JSpecify nullness annotation support
b90a99f
Adds a section to the advanced mapping options chapter describing how
@nonnull, @nullable, @NullMarked, and @NullUnmarked influence null-check
generation, scope resolution, the method-level guard skip for @nonnull
parameters, and the constructor-parameter compile error.
@vmckinney-cainc

vmckinney-cainc commented Apr 14, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

I would humbly ask that you also consider @lombok.NonNull, which is anecdotally very common as well (e.g. 272k github results for import lombok.NonNull vs. 58k for import org.jspecify.annotations.NonNull). Thank you so much for your hard work, I am a big fan of your project!

user99442 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@filiphr

filiphr commented Apr 14, 2026

Copy link
Copy Markdown
Member Author

@vmckinney-cainc I would argue that you are doing an incorrect search for JSpecify. Perhaps a search for import org.jspecify.annotations with 260k results is more relevant. Especially considering the fact that maybe the most common use of JSpecify I would expect is the use of @NullMarked or @NullUmarked on package-info.java which would be applied to all classes in that package.

However, I'm not the biggest user of JSpecify yet, but what I've seen in the ecosystem is the package-info.java usage.

Having said that, I'm not sure that I would like to expand this support to all the other variations of Nullable / NonNull, especially due to the fact that it looks like JSpecify is becoming the industry standard for this.

nealeu and pkernevez reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@user99442

user99442 commented Apr 16, 2026

Copy link
Copy Markdown

First, thank you very much @filiphr for adding support for nullness annotations! This has been a long-expected feature that I’m happy to see.

I would like to support @vmckinney-cainc's feedback on considering additional nullness annotations (other than JSpecify, such as lombok.NonNull or jakarta.annotation.Nonnull), as this could improve the experience of working with third-party library code.

We have just tried out switching our codebase to the SNAPSHOT version of MapStruct built from this branch and seen new errors where source properties are annotated with non‑JSpecify @NonNull annotations (specifically jakarta.annotation.Nonnull) in third‑party code:

Can't map potentially nullable source property "foobar" to `@NonNull` constructor parameter "foobar". Consider adding a defaultValue or defaultExpression.

Because MapStruct currently only recognizes JSpecify annotations, those sources are treated as potentially nullable. For us, this results in many occurrences across mappers and would require adding defaultValue/defaultExpression for every affected property, even though the sources are annotated by their authors.

While I agree that JSpecify is becoming the industry standard, certain libraries may choose to not migrate to it from other annotation types. Adding support for non-JSpecify annotations would reduce the amount of errors an user of the MapStruct library would need to solve when transitioning to the new MapStruct release with improved nullness support.

Therefore, I would like to propose introducing an annotation processor option (e.g. mapstruct.nullness.nonNullAnnotations) to configure the list of considered annotation names. The default would remain JSpecify only, so the behavior is unchanged. For example:

-Amapstruct.nullness.nonNullAnnotations=org.jspecify.annotations.NonNull,jakarta.annotation.Nonnull,lombok.NonNull

This could improve the migration path for projects such as mine that depend on libraries unlikely to adopt JSpecify soon.

If this direction sounds reasonable, we could also discuss adding configurable support for variations of @NullMarked (e.g. JSR-305 @ParametersAreNonnullByDefault) and @Nullable annotations.

Thanks again for your time and for maintaining MapStruct.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pkernevez

pkernevez commented Apr 16, 2026

Copy link
Copy Markdown

First, thank you very much @filiphr for adding support for nullness annotations! This has been a long-expected feature that I’m happy to see.

I would like to support @vmckinney-cainc's feedback on considering additional nullness annotations (other than JSpecify, such as lombok.NonNull or jakarta.annotation.Nonnull), as this could improve the experience of working with third-party library code.

As lombok is a generation tool, isn't it supposed to generate the JSpecify annotation based on its annotations ?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vmckinney-cainc

vmckinney-cainc commented Apr 16, 2026

Copy link
Copy Markdown

@filiphr

I would argue that you are doing an incorrect search for JSpecify

Apologies! Didn't mean to misrepresent the popularity, I'm just not familiar with JSpecify.

JSpecify is becoming the industry standard

I can say at my company in my little niche that in our corporate git we have 1 single match for jspecify. That being said, MapStruct only supporting JSpecify is another reason for us to consider it. I understand giving bias towards open-source projects and don't want to challenge that or pressure you or anything. It's not like closed-source is doing anything to help you :)

@pkernevez

isn't [lombok] supposed to generate the JSpecify annotation[?]

With lombok.addNullAnnotations you can generate jspecify nullability annotations, but only for code which lombok generates. This means that anywhere where we have @lombok.NonNull stays @lombok.NonNull unless we manually migrate.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vmckinney-cainc

vmckinney-cainc commented Apr 16, 2026

Copy link
Copy Markdown

@rRgbMVg7tO

Therefore, I would like to propose introducing an annotation processor option

Caveat with regards to Lombok: @lombok.NonNull has CLASS retention, as opposed to JSpecify and Jakarta's RUNTIME. I think there'd have to be some bytecode introspection or something to look towards Lombok. Ugly stuff.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pkernevez

pkernevez commented Apr 16, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

JSpecify is becoming the industry standard

I can say at my company in my little niche that in our corporate git we have 1 single match for jspecify. That being said, MapStruct only supporting JSpecify is another reason for us to consider it. I understand giving bias towards open-source projects and don't want to challenge that or pressure you or anything. It's not like closed-source is doing anything to help you :)

JSpecify is not just yet another nonNull annotation specification. It's a consortium with a lot of major Java actor https://jspecify.dev/about/
Regarding the adoption it's a quite new project (~1 year from its publication) and it's already adopted in open source like Spring, SpringBoot and Hibernate.
It was complex, ~4 years to create 4 annotations (and the behaviors).
Other annotations like JetBrains, lombok or Jakarta, will probably be deprecated.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

filiphr added 3 commits April 16, 2026 23:55
@filiphr
Require every source-chain accessor to be @nonnull before skipping nu…
e808c28
…ll check

A nested source chain may yield null through any intermediate accessor,
so the chain is only effectively @nonnull when every accessor along it
is @nonnull. Previously only the deepest accessor was consulted, which
caused setter-side null checks to be dropped for chains like
nullableAddress.street where the intermediate was @nullable and the
deepest was @nonnull, passing null into a @nonnull target setter.
@filiphr
Add mapstruct.disableJSpecify option to opt out of JSpecify inference
29e78a0
Converts the static NullabilityUtils helper into an instance-scoped
NullabilityResolver (mirroring AccessorNamingUtils) carrying a boolean
enabled flag derived from the new mapstruct.disableJSpecify processor
option. When the option is set, every JSpecify-driven behavior —
property-level null-check inference, method-level guard skipping,
@NullMarked scope resolution and the @nonnull constructor-parameter
hard error — is suppressed and code generation falls back to the
pre-JSpecify NullValueCheckStrategy-driven behavior.
@filiphr
Document mapstruct.disableJSpecify in the processor options table
01e6d5c
@filiphr

filiphr commented Apr 19, 2026

Copy link
Copy Markdown
Member Author

Thanks @pkernevez for the clarification around JSpecify.

JSpecify is indeed something that took a really long time to get to, and I think the reason has been that there have been all those big players with the different existing annotations. I think with JSpecify 1.0.0 out there, it would become the standard. Considering the fact that large open source projects like Spring and Hibernate have already adopted them, I think it's a matter of fact until other libraries follow suit. IntelliJ also supports them, Kotlin also supports them for their interoperability for reading Java code from Kotlin. They have KT-47417 open for the other way arround, i.e. emit JSpecify annotation from Kotlin itself.

@vmckinney-cainc, I'm not sure which other dependencies you use, but I would be surprised if you do not have jspecify on the transitive path if you are on the latest versions of your dependencies.

@rRgbMVg7tO thanks for your comment.

Because MapStruct currently only recognizes JSpecify annotations, those sources are treated as potentially nullable. For us, this results in many occurrences across mappers and would require adding defaultValue/defaultExpression for every affected property, even though the sources are annotated by their authors.

This was a trigger for me to actually add an opt-out for this using a compiler flag -Amapstruct.disableJSpecify=true.

While I agree that JSpecify is becoming the industry standard, certain libraries may choose to not migrate to it from other annotation types.

To be honest I would rather spend my time in helping those libraries get to JSpecify then spend the time on supporting the different flavors and quirks of the other nullability annoations.

If this direction sounds reasonable, we could also discuss adding configurable support for variations of @NullMarked (e.g. JSR-305 @ParametersAreNonnullByDefault) and @Nullable annotations.

That's the pandora's box I do not want to open.

@vmckinney-cainc not sure what you mean with

Caveat with regards to Lombok: @lombok.NonNull has CLASS retention, as opposed to JSpecify and Jakarta's RUNTIME. I think there'd have to be some bytecode introspection or something to look towards Lombok. Ugly stuff.

If you are thinking that it would be hard for us to read CLASS retention annotation, then no need to think about that. We run during the compilation phase, which means that we can read SOURCE (only for your own code in the same compilation module), CLASS and RUNTIME annotations.


Thanks everyone for your feedback. It is really valuable. However, my decision for this is final. I really do not want to spend time on supporting that the major Java actors have converged on. I would merge this PR in this stance and I would try to do a 1.7.0.Beta2 release soonish so that the community can already start trying it out.

bannmann, user99442, vmckinney-cainc, and thunderhook reacted with thumbs up emoji vmckinney-cainc reacted with heart emoji
All reactions
  • 👍 4 reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@filiphr
filiphr merged commit 9554065 into mapstruct:main Apr 19, 2026
7 of 8 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@filiphr
filiphr deleted the jspecify-nullness-support branch April 19, 2026 20:53
@vmckinney-cainc

vmckinney-cainc commented Apr 20, 2026

Copy link
Copy Markdown

Thanks for all you do @filiphr!! I am very happy to have this feature in any form :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This was referenced May 15, 2026
JSpecify: @NonNull source on collection properties and on container/bean mapping method returns still generates null checks #4056
Closed
#4056 Extend JSpecify support for container mapping and @NonNull return types #4057
Merged
@nealeu

nealeu commented Jun 15, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

@filiphr Are you planning a beta.2 release before final 1.7.0? I think it would be good for this to get a more widely visible release.
[Edit: I just spotted/remembered your comment above, so this is a gentle nudge ;-) ]

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

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

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Support for JSpecify nullness annotations

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@filiphr @nealeu @vmckinney-cainc @user99442 @pkernevez
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.