Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Optional source property that is itself null throws NullPointerException on the generated isPresent() check; NullValueCheckStrategy.ALWAYS is ignored

Đang mở
#4,123 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
68/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
java
Lĩnh vực
tooling

Hướng nghiên cứu

Bắt đầu với reproducer gồm Source.java, SourceMapper.java và Demo.java, sau đó chạy lệnh javac được cung cấp và kiểm tra SourceMapperImpl được tạo ra. So sánh mapper mặc định với biến thể NullValueCheckStrategy.ALWAYS. Hoàn tất khi một nguồn Optional null được xử lý mà không có NPE và strategy được ghi nhận trong tài liệu ảnh hưởng đến presence check được tạo ra.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Expected behavior

When a source property is Optional<T>, the generated presence check should tolerate the Optional reference itself being null, e.g.:

if ( source.getName() != null && source.getName().isPresent() ) {
    name = source.getName().get();
}

or, at minimum, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS should produce that guard, since that is the documented knob for "always null-check the source before using it".

Actual behavior

The generated code calls isPresent() on the raw getter result, with no null check:

@Override
public Target toTarget(Source source) {
    if ( source == null ) {
        return null;
    }

    String name = null;

    if ( source.getName().isPresent() ) {
        name = source.getName().get();
    }

    Target target = new Target( name );

    return target;
}

If the Optional property is null (which is a plain reference like any other, and is what many object builders and deserializers leave it as), this throws:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because the return value of "repro.Source.getName()" is null
	at repro.SourceMapperImpl.toTarget(SourceMapperImpl.java:20)
	at Demo.main(Demo.java:8)

The 1.7.0.Beta1 release notes say this is intentional: "Note that we are not doing any null checks for the optional properties. Instead, we do a check if the optional is present or not and map it." The problem is that this makes the generated mapper unsafe for any source object that can hold a null Optional, and there is no configuration to opt out of it.

NullValueCheckStrategy.ALWAYS has no effect here. With the identical model and

@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface SourceMapperAlways
{
    Target toTarget( Source source );
}

the generated body is byte-for-byte the same as with the default strategy — still a bare source.getName().isPresent(). That seems like the core issue: the setting that exists precisely to force source null checks is silently ignored for Optional properties.

This is not a hypothetical input. Two common cases where a null Optional source field arises:

  1. Generated builders. Lombok's @Builder leaves an unset Optional component as null, not Optional.empty() (verified below with Lombok 1.18.48).
  2. Jackson deserialization, where null (field absent from the JSON) and Optional.empty() (field explicitly null in the JSON) are deliberately distinguished — this exact scenario was raised in https://github.com/mapstruct/mapstruct/issues/674#issuecomment-4291090849 but there is no tracking issue for it, hence this report.

This is distinct from #4111, which is about the presence check being omitted entirely for Optional<Map> / Optional<Collection>; here the presence check is generated, but it is itself unguarded.

Workaround (works, but requires one @Condition method per mapper/type):

@Condition
default boolean isSet( Optional<String> value )
{
    return value != null && value.isPresent();
}

which produces if ( isSet( source.getName() ) ).

Steps to reproduce the problem

Minimal case, no Lombok, plain javac, no other annotation processors.

Source.java:

package repro;

import java.util.Optional;

public class Source
{
    private Optional<String> name;

    public Optional<String> getName()
    {
        return name;
    }

    public void setName( Optional<String> name )
    {
        this.name = name;
    }
}

Target.java:

package repro;

public record Target( String name )
{ }

SourceMapper.java:

package repro;

import org.mapstruct.Mapper;

@Mapper
public interface SourceMapper
{
    Target toTarget( Source source );
}

Demo.java:

import repro.*;

public class Demo
{
    public static void main( String[] args )
    {
        Source source = new Source();
        System.out.println( new SourceMapperImpl().toTarget( source ) );
    }
}
javac -cp mapstruct-1.7.0.Beta2.jar \
      -processorpath mapstruct-processor-1.7.0.Beta2.jar:mapstruct-1.7.0.Beta2.jar \
      -s gen -d out src/repro/*.java src/Demo.java

Adding a second mapper with nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS over the same types generates an identical method body.

Lombok variant, showing that this is reachable without ever writing null by hand:

package repro;

import java.util.Optional;
import lombok.Builder;

@Builder
public record Source( String id, Optional<String> name )
{ }
Source source = Source.builder().id( "1" ).build();
System.out.println( "name field is: " + source.name() );   // => name field is: null
new SourceMapperImpl().toTarget( source );                  // => NPE
name field is: null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because the return value of "repro.Source.name()" is null
	at repro.SourceMapperImpl.toTarget(SourceMapperImpl.java:22)
	at Demo.main(Demo.java:9)
MapStruct Version

1.7.0.Beta2, Java 25.0.2 (Eclipse Adoptium), javac. Lombok 1.18.48 for the second variant.

Ngôn ngữ chính
Java
Star
7.7k
Fork
1.1k
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của mapstruct/mapstruct

Tất cả issue của mapstruct/mapstruct

Issue tương tự

Thêm issue về Java

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.