Skip to content

Conversation

@filiphr
Copy link
Member

@filiphr filiphr commented Jan 25, 2026

This PR builds on top of PR #3183 and resolves #674.

The most notable things in the additional rework of #3183 are the following:

  • Implement mapping as part of Bean Mapping
  • Always assume that optionals are not null, i.e. we can always call isPresent() / isEmpty() on an Optional
  • Add support for conversions e.g. Optional<String> -> String, Optional<Long> -> String etc.

@robotmrv
Copy link

regarding

Always assume that optionals are not null, i.e. we can always call isPresent() / isEmpty() on an Optional

this looks like a breaking change
see case with jackson + Optional deserialization #3183 (comment)
In this case it is possible that Optional is null and probably could break existing users after update to a new version. Or at least this feature will be unusable for such case.

Is it possible to add config option for mapping like this

Source Type Target Type Source Value Target Value
Object Optional null null
Optional Object null null
Optional Optional null null

i.e. null maps to null always for objects
?

@filiphr
Copy link
Member Author

filiphr commented Jan 26, 2026

this looks like a breaking change

Breaking change compare to what? Currently we do not have support for optional at all.
Can you provide certain things that work in the moment, but won't keep working?

I personally really dislike using Optional as input parameter and / or a field, IntelliJ even constantly complains about that and in addition to that the Optional javadoc itself says

Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

see case with jackson + Optional deserialization

This is not because Jackson does that, it's more because Jackson will not invoke the setter if there is nothing in the json.

@robotmrv
Copy link

breaking in terms that mapstruct usually checks source for null. But breaking was a wrong word maybe unexpected/surprising because null check is a boilerplate code and using mapstruct is the main use case for null safe boilerplate mapping code generation.
And I am not sure how it would work with generic mappers for Optional like

@Mapper
public interface OptionalMapper {

    default <E> E unwrapFromOptional(Optional<E> optional) {
        return optional == null
            ? null
            : optional.orElse(null);
    }

    default <E> Optional<E> wrapToOptional(E value) {
        return value == null
            ? null
            : Optional.of(value);
    }
}

I personally really dislike using Optional as input parameter and / or a field

I don't like it either, but ther are a lot of code that used it this way and at least part of the mapstruct users use is it this way and cannot change it due to their API contracts.

This is not because Jackson does that, it's more because Jackson will not invoke the setter if there is nothing in the json.

This is just implementaion detail and the main point is that Optional is used as a container for patch requests. Because Optional is part of the jdk and does not require additional dependencies and because jackson supports such case with Optional out of the box.

An additional use case is cloning objects using mappers that map to the same type, and you want this to be symmetrical, but this is not possible with the null -> Optional.empty() asymmetric approach.

@filiphr
Copy link
Member Author

filiphr commented Jan 26, 2026

I don't like it either, but ther are a lot of code that used it this way and at least part of the mapstruct users use is it this way and cannot change it due to their API contracts.

If MapStruct users are using this right now it means that they have custom methods where this is being done, this means that this will keep working like that. User defined methods always have a precedence over MapStruct internals.

An additional use case is cloning objects using mappers that map to the same type, and you want this to be symmetrical, but this is not possible with the null -> Optional.empty() asymmetric approach.

That stays the same. Have a look at the OptionalSameTypeMapperImpl in the PR. I generates:

constructorOptionalToOptional = source.getConstructorOptionalToOptional();

When the optional can be directly assigned, it is assigned without a presence check.

And I am not sure how it would work with generic mappers for Optional like

I added some tests like that.

I am not super keen on adding something that only small number of users is going to use, especially if it can be achieved with custom methods. I would actually prefer to try to keep it simple and in the spirit of how optional is supposed to be used without any warnings from the IDE. If I treat Optional as null then IntelliJ gives me a warning about it, same when I use it as a method parameter.

@Override
public String toString() {
if ( optionalType.getFullyQualifiedName().equals( "java.util.Optional" ) ) {
return getAssignment() + ".get()";
Copy link

Choose a reason for hiding this comment

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

Even since there is no difference in the result, I just wanted to mention that
the (official) preferred alternative instead of get() is orElseThrow().

(Not sure if this is the correct place for this comment; it just looked the most promising for me.)

Copy link
Member Author

Choose a reason for hiding this comment

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

orElseThrow() is there from Java 10 and we aim to support Java 8 code generation. It could be an improvement that we can do (like we are doing with the isEmpty() check), but I would do it separately from this PR. I've opened #3976 as a follow-up for this

@filiphr filiphr merged commit 9923f78 into mapstruct:main Jan 30, 2026
5 checks passed
@filiphr filiphr deleted the optional-support branch January 30, 2026 11:25
@sanmibuh
Copy link

Good job!!!!!

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.

Support Java 8 java.util.Optional type

5 participants