Search code examples
javaspringvalidationspring-security

Define Order between Jakarta Validations and Custom Validations


When calling /orders/{order_id}/discounts/{discount_id} :

  • Request is authenticated through a filter chain I configured
  • DiscountValidator method isValid() is executed,
  • @Valid annotation is applied, validating all annotations like @NotNull, @Digits, etc..
  • @PreAuthorize is executed.

The thing is I want Jakarta annotations to validate my fields first, and then to run custom validator (DiscountValidator) assuming discountValue for example, is not null.

  • Controller Method Signature

    @PreAuthorize("hasRealmRoles('INTERNAL_USER','ROBOTIC_USER') or orderBelongsToAccount(#orderIdAsString)")
    @PutMapping(path = "orders/{order_id}/discounts/{discount_id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<OrderDiscountDto> updateOrderDiscount(@Valid @RequestBody OrderDiscountRequest orderDiscountRequest,
                                                                @UUIDConstraint @PathVariable("order_id") String orderIdAsString,
                                                                @UUIDConstraint @PathVariable("discount_id") String discountIdAsString);

  • OrderDiscountRequest pojo
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    @Builder
    @ValidDiscount
    public class OrderDiscountRequest {
        @Schema(description = "The decimal amount, deducted from order's total value", name = "discount_value",
                example = "31.00", type = "string", format = "string")
        @JsonProperty("discount_value")
        @DecimalMin("0.0")
        @Digits(integer = 10, fraction = 2)
        @NotNull(message = "Discount value cannot be null. Provide field value.")
        private BigDecimal discountValue;
    
        @Schema(description = "Object containing info regarding discount policy", name = "policy",
                implementation = DiscountPolicy.class, enumAsRef = true)
        @JsonProperty("policy")
        @NotNull(message = "Discount Policy cannot be null. Provide field value.")
        private DiscountPolicy policy;
    }
  • ValidDiscount annotation

    @Constraint(validatedBy = DiscountValidator.class)
    @Target({ElementType.PARAMETER, ElementType.TYPE, ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ValidDiscount {
        String message() default "Invalid discount request information";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }

  • DiscountValidator class

    @Slf4j
    public class DiscountValidator implements ConstraintValidator<ValidDiscount, OrderDiscountRequest> {
    
        @Override
        public boolean isValid(OrderDiscountRequest discountRequest, ConstraintValidatorContext context) {
    
            validatePercentagePolicy(discountRequest);
    
            return true;
        }
    
        private void validatePercentagePolicy(OrderDiscountRequest discountRequest) {
            if (DiscountPolicy.PERCENTAGE.equals(discountRequest.getPolicy()) &&
                    discountRequest.getDiscountValue().compareTo(MAX_PERCENTAGE) == 1) {
                log.warn("Discount percentage value greater than 100.00");
                throw new ConstraintViolationException(ValidationMessages.ORDER_DISCOUNT_PERCENTAGE_VALUE_EXCEEDED, new HashSet<>());
            }
        }
    }

  • Configuration class, to push validations before authorization process (because authorization needs a present order resource for requested order_id)
@Configuration
public class ValidationConfiguration {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setBeforeExistingAdvisors(true);
        return processor;
    }
}

Solution

  • Add annotation @GroupSequence to validate using Jakarta annotations first, before custom validator.
    
    @GroupSequence({OrderDiscountRequest.class, DiscountValidator.class})
    public class OrderDiscountRequest {