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")
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.