Skip to content

Bug: Bean validation not working on default interface methods #48280

@cdprete

Description

@cdprete

🐞 Bug report (please don't include this emoji/text, just add your details)

Hello.
It seems like that custom constraints are simply ignored in version 3.5.8 when applied to parameters of interface default methods.
I've the following custom constraint:

@Documented
@Retention(RUNTIME)
@Target({TYPE, RECORD_COMPONENT, PARAMETER, METHOD})
@Constraint(validatedBy = ValidAdjustmentDateValidator.class)
public @interface ValidAdjustmentDate {
    String message() default "Invalid date combination";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

import com.google.type.Date;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Year;

public class ValidAdjustmentDateValidator implements ConstraintValidator<ValidAdjustmentDate, Date> {
    private static final Logger logger = LoggerFactory.getLogger(ValidAdjustmentDateValidator.class);

    static final int MIN_YEAR = LocalDate.EPOCH.getYear();

    @Override
    public boolean isValid(Date adjustmentDate, ConstraintValidatorContext context) {
        String validationErrorMessage = null;
        int maxYear = getMaxYear();
        if (adjustmentDate.getYear() < MIN_YEAR || adjustmentDate.getYear() > maxYear) {
            validationErrorMessage = "Year must be between %d and %d: year=%d".formatted(MIN_YEAR, maxYear, adjustmentDate.getYear());
        } else {
            try {
                //noinspection ResultOfMethodCallIgnored -> needed just to validate the date itself
                LocalDate.of(adjustmentDate.getYear(), adjustmentDate.getMonth(), adjustmentDate.getDay());
            } catch (DateTimeException e) {
                logger.debug(e.toString(), e);
                validationErrorMessage = "Invalid date combination: year=%d, month=%d, day=%d".formatted(adjustmentDate.getYear(), adjustmentDate.getMonth(), adjustmentDate.getDay());
            }
        }

        if(validationErrorMessage != null) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(validationErrorMessage).addConstraintViolation();
        }

        return validationErrorMessage == null;
    }

    public static int getMaxYear() {
        return Year.now().plusYears(1).getValue();
    }
}

which is applied on the following Converter interface:

@Validated
@Mapper(config = MappingConfiguration.class, uses = ListingIdentifierToListingConverter.class)
public interface GetAdjustmentsRequestToAdjustmentRequestConverter extends Converter<GetAdjustmentsRequest, AdjustmentRequest> {
    @Valid
    @Override
    @Mapping(source = "listingsList", target = "listings")
    @Mapping(expression = "java(toLocalDate(source.getToDate()))", target = "to")
    @Mapping(expression = "java(toLocalDate(source.getFromDate()))", target = "from")
    AdjustmentRequest convert(GetAdjustmentsRequest source);

    default LocalDate toLocalDate(@ValidAdjustmentDate Date date) {
        return date == null ? null : LocalDate.of(date.getYear(), date.getMonth(), date.getDay());
    }
}

The implementation that gets generated looks, to me, correct:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: javac, environment: Java 25 (Azul Systems, Inc.)"
)
@Component
public class GetAdjustmentsRequestToAdjustmentRequestConverterImpl implements GetAdjustmentsRequestToAdjustmentRequestConverter {

    private final ListingIdentifierToListingConverter listingIdentifierToListingConverter;

    @Autowired
    public GetAdjustmentsRequestToAdjustmentRequestConverterImpl(ListingIdentifierToListingConverter listingIdentifierToListingConverter) {

        this.listingIdentifierToListingConverter = listingIdentifierToListingConverter;
    }

    @Override
    public AdjustmentRequest convert(GetAdjustmentsRequest source) {
        if ( source == null ) {
            return null;
        }

        Set<Listing> listings = null;
        boolean cumulateDailyAdjustments = false;

        listings = listingIdentifierListToListingSet( source.getListingsList() );
        cumulateDailyAdjustments = source.getCumulateDailyAdjustments();

        LocalDate to = toLocalDate(source.getToDate());
        LocalDate from = toLocalDate(source.getFromDate());

        AdjustmentRequest adjustmentRequest = new AdjustmentRequest( listings, from, to, cumulateDailyAdjustments );

        return adjustmentRequest;
    }

    protected Set<Listing> listingIdentifierListToListingSet(List<ListingIdentifier> list) {
        if ( list == null ) {
            return new LinkedHashSet<Listing>();
        }

        Set<Listing> set = LinkedHashSet.newLinkedHashSet( list.size() );
        for ( ListingIdentifier listingIdentifier : list ) {
            set.add( listingIdentifierToListingConverter.convert( listingIdentifier ) );
        }

        return set;
    }
}

For constraints defined on objects themselves like, for example:

import org.hibernate.validator.constraints.Range;

public record Listing(@Range(min = 1, max = MAX_UINT) long valor, @Range(min = 1, max = MAX_USHORT) int marketCode, @Range(min = 1, max = MAX_USHORT) int currencyCode) {
    static final long MAX_UINT = (1L << Integer.SIZE) - 1;
    static final int MAX_USHORT = (1 << Short.SIZE) - 1;
}

the validation is working as expected.
I would be expecting to not be working given that the method is within the same class being proxied, but MethodValidationPostProcessor is there and it should therefore work to my knowledge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions