2

I am using Spring Boot (2.7.6) and javax.validation and swagger v3.

My request JSON body of this form

{
    "itemDescription":"vehicle",
    "itemVersion":0,
    "itemCode":{
        "codeType":"1",
        "codeValue":"1111"
    }
}

The above JSON body is represented by 2 models ItemRequest and ItemCode as:

import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ItemRequest {

    @NotNull
    @Schema(description = "Item description"
        , requiredMode = Schema.RequiredMode.REQUIRED)
    private String itemDescription;
    
    @NotNull
    @Schema(description = "Unique Item Code consisting of codeType integer and codeValue string"
        , requiredMode = Schema.RequiredMode.NOT_REQUIRED)  
    private ItemCode itemCode;
    
    @Min(value = 0)
    @Schema(description = "Item version'
        , requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private int itemVersion;
}

, and its nested JSON model ItemCode:

import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotNull;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class ItemCode {

    @NotNull
    @Schema(description="codeType can be 1, 2, or 3"
        , requiredMode = Schema.RequiredMode.REQUIRED))
    private int codeType;
    
    @NotNull
    @Schema(description="Unique itemCode identifier. Can start with 0.  If codeType=1, 3-digits long, if codeType=2, 4-digits long, if codeTpye=3, 5-digits long.",
        , requiredMode = Schema.RequiredMode.REQUIRED)
    private String codeValue;  // for example 021, 5455, 05324
}

How do I validate ItemCode field in ItemRequest model so that validation does what descriptions for ItemCode state?

cd491415
  • 823
  • 2
  • 14
  • 33
  • Define a ConstraintValidator for your DTO. The regular spring validations will not work here since you have constraint validations depending on the values of other constraint values. – Superchamp May 02 '23 at 20:50
  • Thanks. How can I throw a custom exception like `MyCustomException` from `ConstraintValidator` instead of getting `MethodArgumentNotValidException`? – cd491415 May 02 '23 at 22:39
  • In spring, it is always better to use a controller advice to handle exceptions. This blog will be helpful for you to write a exception handler - https://verley.dev/blog/constraint-validation-in-spring-boot-microservices/ – Superchamp May 02 '23 at 23:17
  • @Superchamp Yes, I am already using ControllerAdvice for handling errors. My problem is that in isValid() method of my ConstraintValidator, I am trying to throw my custom exception `MyCustomException` if validation fails, but `MyCustomException` ExceptionHandler is never reached. Instead it lands in my pre-existing ExceptionHandler for `MethodArgumentNotValidException`. In the above url you shared, the example is handling `MethodArgumentNotValidException`, not a custom exception. – cd491415 May 03 '23 at 00:10
  • 1
    Are you saying that you are throwing a custom exception inside the isValid method and still seeing a MethodArgumentNotValidException? If you return a false from the method then spring will automatically handle that scenario with a default exception. A custom exception may not always be necessary - you could also use the buildConstraintViolationWithTemplate to add your custom message. – Superchamp May 03 '23 at 11:29

1 Answers1

2

The way I solved it thanks to suggestions from @Superchamp

Create a custom annotation

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ItemCodeValidator.class)
@Documented
public @interface ValidItemCode {

    String message() default "Invalid ItemCode for ItemCode.codeType for ItemCode.codeValue: ${validatedValue}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

Add custom validation logic:

public class ItemCodeValidator implements ConstraintValidator<ValidItemCode, ItemCode> {

    @Override
    public boolean isValid(ItemCode itemCode, ConstraintValidatorContext context) {

        if (itemCode == null) {
            return false;
        }

        switch (itemCode.getCodeType()) {
            case 1:
               return validateItemCode1(itemCode);
            case 2:
                return validateItemCode2(itemCode);
            case 3:
                return validateItemCode3(itemCode);
            default:
                return false;
        }
    }

    private boolean validateItemCode1(Key key) {
       // do your validation
    }

    private boolean validateItemCode2(Key key) {
        // do your validation
    }

    private boolean validateItemCode3(Key key) {
        // do your validation
    }
}

Add my custom annotation to the field in question:

...
@NotNull
@Schema(description = "Unique Item Code consisting of codeType integer and codeValue string"
        , requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@ValidItemCode 
private ItemCode itemCode;
...
cd491415
  • 823
  • 2
  • 14
  • 33