Search code examples
javaspringspring-bootvalidation

How to validate a string input passing to float type dto


How to validate a string input passing to float type dto?

I can pass any type of float values from 1.0 to 99.99. However, when I pass through 99A.99, it returns error 400 Bad Request

JSON parse error: Cannot deserialize value of type `java.lang.Float` from String \"99A.99\": not a valid `Float` value

DTO

public class BookDTO {

    @NotNull
    @DecimalMin(value = "0.00", inclusive = true, message = "Price should not be less than 0.00")
    @DecimalMax(value = "99.99", inclusive = true, message = "Price should not be more than 99.99")
    private Float price;

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }
}

I want to make sure that all inputs passing is a float type. Have not really found any luck checking internet. I checked chatgpt and it was repeatedly suggested to do Create a Custom Validator Annotation from string to float.

Below is what I tried:

ValidFloatFormat

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FloatFormatValidator.class)
public @interface ValidFloatFormat {

    String message() default "Invalid float format";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

FloatFormatValidator

public class FloatFormatValidator implements ConstraintValidator<ValidFloatFormat, Float> {

    @Override
    public boolean isValid(Float value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // Let @NotNull handle null values
        }

        try {
            Float.parseFloat(String.valueOf(value)); // Try parsing to check format
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

DTO (updated)

    @NotNull
    @ValidFloatFormat(message="INVALID FLOAT")
    @DecimalMin(value = "0.00", inclusive = true, message = "Price should not be less than 0.00")
    @DecimalMax(value = "99.99", inclusive = true, message = "Price should not be more than 99.99")
    private Float price;

I am still getting the same bad request error.

Additional information:

Full response:

{
    "timestamp": "2024-07-11T14:18:03.787+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "JSON parse error: Cannot deserialize value of type `java.lang.Float` from String \"9a9.99\": not a valid `Float` value",
    "path": "/api/books"
}

Expected sample response (simplified):

{
    "timestamp": "2024-07-11T14:29:40.473398987",
    "status": 400,
    "error": "BAD_REQUEST",
    "errors": {
        "genre": [
            "size must be between 5 and 10"
        ],
        "title": [
            "size must be between 5 and 20",
            "Title should be alphanumeric Proper Noun case"
        ]
    }
}

Reason why the expected response is simplified instead of the default (very long) @Valid response:

Controller

    public ResponseEntity<?> createBook(@Valid @RequestBody BookDTO bookDTO, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            ValidationErrorResponse validationErrorResponse = new ValidationErrorResponse();
            validationErrorResponse.setStatus(HttpStatus.BAD_REQUEST.value());
            validationErrorResponse.setError(HttpStatus.BAD_REQUEST.name());
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                validationErrorResponse.addError(fieldError.getField(), fieldError.getDefaultMessage());
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(validationErrorResponse);
        }

        BookDTO createdBook = bookService.createBook(bookDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdBook);
    }

ValidationErrorResponse

public class ValidationErrorResponse {

    private Map<String, List<String>> errors = new HashMap<>();

    public Map<String, List<String>> getErrors() {
        return errors;
    }

    ... // other columns

    public void addError(String field, String errorMessage) {
        errors.computeIfAbsent(field, k -> new ArrayList<>()).add(errorMessage);
    }
}

For the invalid float, I would like to return the default message that was set in the DTO ("INVALID FLOAT")


Solution

  • After going at it back and forth, the only change I made that works is variable type from Float to String

    private Float price;
    

    private String price;
    

    Then change all affected parts of the code. I guess Java/Spring Boot throws an exception message that Float types could not accept String as an input (even before custom validation is executed). I was expecting that my custom validation comes first before spring boot argues that it is not possible.

    Moving forward, I might have to add a separate class for this (Validation Class) where it will accept everything in String or Object then validate it before passing to DTO.