JSpecify nullness support, configurable mapper accessibility, and much more: MapStruct 1.7.0.Beta2 is out
It’s my pleasure to announce the second Beta release of MapStruct 1.7.
The new release comes with some new functionality and many quality of life improvements to the generated code, e.g.:
- JSpecify nullness annotation support
- Configurable accessibility for the generated mapper class
- Built-in conversion between
URIandString - Modernized generated code (switch expressions for value mappings, diamond operator, multi-catch)
Altogether, not less than 45 issues were fixed for this release.
This would not have been possible without our fantastic community of contributors:
A very special welcome to @hduelme, who joined us as a regular contributor in this release cycle and is behind a large portion of the improvements that ship with 1.7.0.Beta2. Thank you for the dedication and the steady stream of high-quality contributions!
Thank you everyone for all your hard work!
We are also accepting donations through Open Collective or GitHub. We’d like to thank all the supporters that supported us with donations in this period:
- adesso SE
- Cybozu
- @foal
- Frederik Hahne
- Lansana
- Mercedes-Benz Group
- Miikka Ylätalo
- Neale U
- @pmkyl
- Solum Financial
- VEMA eG
- znight1020
Enough of the pep talk, let’s take a closer look at some of the new features and enhancements!
JSpecify nullness annotation support
MapStruct now understands JSpecify nullness annotations and uses them to decide whether to emit null checks in the generated code.
The annotations @NonNull, @Nullable, @NullMarked and @NullUnmarked from org.jspecify.annotations are detected automatically.
Property-level null checks
- A
@NonNullsource value is treated as guaranteed non-null, so the null check is skipped. - A
@NonNulltarget always gets a null check, to protect it from a null assignment. - In all other cases MapStruct falls back to the existing
NullValueCheckStrategy.
The same rules apply to collection-typed properties, so a @NonNull List<String> source no longer triggers a redundant null check around the wrapper.
Safety guards like defaultValue, unboxing checks and NullValuePropertyMappingStrategy are still applied.
Method-level source parameter and return type
If the source parameter of a mapping method is @NonNull, MapStruct skips the method-level null guard at the beginning of the generated method.
The same is true for container mapping methods (Iterable → Iterable, Map → Map, Stream → Stream), which now honor JSpecify on their source parameter rather than always emitting an if ( source == null ) return null; guard.
When a mapping method’s return type is @NonNull, MapStruct will no longer emit return null; (which would violate the return contract), and instead applies NullValueMappingStrategy.RETURN_DEFAULT semantics. This applies consistently across bean, iterable, map and stream mapping methods.
e.g.
@NullMarked
@Mapper
public interface CustomerMapper {
CustomerDto map(Customer customer);
}
Will generate something like:
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {
@Override
public CustomerDto map(Customer customer) {
CustomerDto customerDto = new CustomerDto();
// ...
return customerDto;
}
}
Note that there is no if ( customer == null ) guard, because customer is effectively @NonNull within the @NullMarked scope, and the method does not return null because the return type is also effectively @NonNull.
@NullMarked / @NullUnmarked scope
- Inside a
@NullMarkedclass or package, unannotated reference types are effectively@NonNull. @NullUnmarkedreverts to unknown nullability within its scope.@Nullableon a specific type always overrides the surrounding scope.- The scope is resolved by walking the enclosing element chain (method → class → outer class → package → module).
Constructor parameters
When a potentially nullable source is mapped to a @NonNull constructor parameter, and there is no defaultValue, MapStruct now raises a compile error.
A null check at that point would leave the variable at null, which would violate the contract of the constructor.
Opting out
If you want to keep the pre-1.7.0.Beta2 behavior, you can disable JSpecify inference with the processor option mapstruct.disableJSpecify=true.
When set, all JSpecify-driven decisions (property-level inference, method-level guard skipping, scope resolution and the constructor-parameter error) are suppressed and code generation falls back to the NullValueCheckStrategy-driven behavior.
For more details have a look at the JSpecify nullness annotations section of the reference guide.
Configurable accessibility for the generated mapper class
By default, the generated mapper implementation has the same accessibility (public or package-private) as the interface or abstract class carrying the @Mapper annotation.
In some cases — typically when relying on a DI framework like Spring — you may want the generated implementation to be package-private, even when the mapper interface itself is public.
A new accessibility attribute on @Mapper and @MapperConfig (along with a new ClassAccessibility enum) gives you control over this:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,
accessibility = ClassAccessibility.PACKAGE_PRIVATE)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
Possible values are:
ClassAccessibility.DEFAULT- mirror the accessibility of the annotated interface or class (the existing behavior).ClassAccessibility.PUBLIC- the generated mapper ispublic.ClassAccessibility.PACKAGE_PRIVATE- the generated mapper has no visibility modifier.
For more details have a look at the Class accessibility section of the reference guide.
Switch expressions for value mappings
Value mapping methods (such as enum-to-enum or String-to-enum mappings) now generate a Java 14 switch expression instead of the classic switch statement, when the target compiler level is JDK 14 or later.
Instead of generating:
switch ( b ) {
case "A": a = ValueMapper.A.A;
break;
case "B": a = ValueMapper.A.B;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + b );
}
MapStruct will now generate:
ValueMapper.A a = switch ( b ) {
case "A" -> ValueMapper.A.A;
case "B" -> ValueMapper.A.B;
default -> throw new IllegalArgumentException( "Unexpected enum constant: " + b );
};
When the switch has only a default branch, MapStruct will skip the switch altogether and assign the default target directly.
Enhancements
- Add a built-in conversion between
java.net.URIandString, similar to the existing conversions forUUIDandURL. - Add built-in conversions between
ZonedDateTime,OffsetDateTime,InstantandLocalDateTime, so thesejava.timetypes now convert directly instead of going throughXMLGregorianCalendar. - Use the diamond operator (
<>) in generated code instead of repeating the generic type arguments. - Use Java’s multi-catch in generated code when several exceptions are handled the same way, instead of multiple separate
catchblocks. - Use
URI.create(String).toURL()instead of the deprecatednew URL(String)constructor inStringtoURLconversions, avoiding deprecation warnings on Java 20+.
Enhancements with behavior changes
- Mappings from
Collection<? super Type>toCollection<? extends Type>(and the correspondingStreamcase) now result inPROPERTYMAPPING_MAPPING_NOT_FOUNDinstead of generating code that fails to compile due to an unchecked cast.
Download
This concludes our tour through MapStruct 1.7 Beta2. If you’d like to try out the features described above, you can fetch the new release from Maven Central using the following GAV coordinates:
- Annotation JAR: org.mapstruct:mapstruct:1.7.0.Beta2
- Annotation processor JAR: org.mapstruct:mapstruct-processor:1.7.0.Beta2
Alternatively, you can get ZIP and TAR.GZ distribution bundles - containing all the JARs, documentation etc. - from GitHub.
If you run into any trouble or would like to report a bug, feature request or similar, use the following channels to get in touch:
- Get help in our GitHub Discussions or StackOverflow
- Report bugs and feature requests via the issue tracker
- Follow @GetMapStruct on Twitter