How to validate type 'java.lang.Boolean' in Spring boot
Asked Answered
H

6

14

I have a boolean field which I want to validate to have only "true" or "false" as value(without quotes). But this field is also allowing "true" or "false" as value(with quotes) which I want to restrict.

I tried to use @Pattern annotation to match these values. But I have the following error:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.Pattern' validating type 'java.lang.Boolean'. Check configuration for 'restartable'

My code is:

@Pattern(regexp = "^(true|false)$", message = "restartable field allowed input: true or false")
private Boolean restartable;

What should i do? Is there some other way to overcome this issue? Thanks in advance.

Hastie answered 30/5, 2019 at 15:29 Comment(6)
How could a boolean possibly have another value? A Boolean is not a string. It doesn't contain characters.Proclivity
Pattern annotation checks a CharSequence not a Boolean Object according to the documentation: docs.oracle.com/javaee/7/api/javax/validation/constraints/… You would need to change your type to a String in order to use Pattern. Alternatively you may create your own validator or check whether there is a boolean validator alreadyDrucie
@JBNizet But it's accepting "true" as a string.Hastie
@Hastie no it doesn't. Those annotations are bean-validation annotations. They are used to validate your bean. So all the validator sees is an object of type Boolean. It doesn't see any string. What transforms the string to a boolean has nothing to do with bean validation. It's the JSON parser. If you want to reject string values for boolean fields, you need to configure your JSON parser, not bean validation.Proclivity
@JBNizet How can I configure Json parser to handle this? I’m using Jackson ObjectMapper class.Hastie
By writing a custom deserializer for booleans, probably.Proclivity
G
19

you can use @NotNull and @AssertTrue or @AssertFalse assertions present in javax.validation if the goal is to enforce non null and truthy value.

https://docs.oracle.com/javaee/7/tutorial/bean-validation001.htm

@NotNull
@AssertTrue
private Boolean restartable;

Not Null annotation would suffice if the value can be either true or false as booleans in Java can only have two values.

Glacial answered 30/5, 2019 at 15:40 Comment(5)
But this will restrict the field to only have true. Right?Hastie
Right, sorry If I misunderstood, but if the value can be true or false, not null validation would suffice as if you try to pass string or number to that variable, it will fail.Glacial
I tried but it allows "true" as a string, which I want to restrict.Hastie
I assume you are using this class as a deserialized version of web request. If that's the case all parameters and body of a request are string. The java bean would construct appropriate data type parsing that string only.Glacial
Not what the OP was asking forGassaway
S
7

I had a similar issue with form validation of a Boolean value, where I technically only wanted the client to pass either true or false, no other values, to ensure they understood they were actually passing those values, and not passing an integer and my code running based on the interpretted value of false (Boolean.valueOf() returns false for basically anything besides true).

To clarify the problem statement, since some people seem a little confused, boolean validation fails here because they can pass

{...
"value":8675309,
...}

where value is SUPPOSED to be boolean (but clearly passed as int), however the validator/converter just runs Boolean.valueOf() on the passed in object, which in this case would result in false, which could result in downstream logic and changes that the client was NOT expecting (ie if boolean value was something like keepInformation, that above scenario could result in a user losing all of their information because the form wasn't correctly validated, and you possibly being on the hook since the client didn't "technically" say "keepInformation":false)

Anyways, in order to combat this, I found the easiest way was to store the boolean as a String like such

    @NotNull
    @Pattern(regexp = "^true$|^false$", message = "allowed input: true or false")
    private String booleanField;

I've tested this regex and it will ONLY pass for "value":true/"true", or "value":false/"false", it will fail on extra quotes ("value":"\"true\""), whitespace ("value":"true "), and anything else that doesn't match what I put above.

Then in your own getter in that data object you can run the boolean conversion yourself for downstream use.

    public boolean isSomething() {
        return Boolean.valueOf(booleanField);
    }
Simmon answered 23/7, 2020 at 17:40 Comment(0)
E
1

Why are you using a text validation on a boolean field? that's what the error is stating.

I think it would make more sense to check if it's null or not (use @NotNull); if it's non-null then surely it's a valid boolean, by definition it can't have any other value besides true or false. On the other hand, if it's null then that's probably the error that you want to detect.

Everhart answered 30/5, 2019 at 15:34 Comment(2)
If you're trying to avoid null values, just use the primitive boolean instead of the class Boolean as primitives can't actually be null.Senlac
That is true, however any value as a String will pass through valueOf on parsing (i believe). So the Boolean value will be available, but the value "this is a test" will still yield false rather than throw the appropriate error.Drucie
E
1

If you just want to know which field is validation failed, you can set a GlobalExceptionHandler to catch ServerWebInputException, then get rootcause from ServerWebInputException which is instanceof InvalidFormatException, this exception contains validation failed field info and human-readable reason.

Ecdysiast answered 1/9, 2021 at 3:56 Comment(1)
Please provide additional details in your answer. As it's currently written, it's hard to understand your solution.Yuma
S
1

This may help:

public class Person {
  @JsonDeserialize(using = CustomBooleanDeserializer.class)
  private Boolean smart;
}

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import java.io.IOException;

public class CustomBooleanDeserializer extends JsonDeserializer<Boolean> {
    @Override
    public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getText();
        if(value != null && !value.equals("true") && !value.equals("false")) {
            throw new InvalidFormatException(p, "not boolean value (must be true or false only)", value, Boolean.class);
        }
        return Boolean.valueOf(value);
    }
}
Scherzando answered 1/9, 2022 at 8:26 Comment(0)
W
0

You can use a exception handler to catch HttpMessageNotReadableException and check if is caused from InvalidFormatException.

@RestControllerAdvice
public class ControllerExceptionHandler {

  @ExceptionHandler(HttpMessageNotReadableException.class)
      @ResponseStatus(value = HttpStatus.BAD_REQUEST)
      public ErrorMessage exceptionHandler(HttpMessageNotReadableException e, 
  WebRequest request) {
        if(e.getRootCause() instanceof InvalidFormatException){
             // do something
        }
  }

}
        

OR make a custom Deserializer for the error message output.

Reference: https://langinteger.github.io/2021/08/02/spring-web-data-binding-and-validation/

class Person {
  @JsonDeserialize(using = MyIntDeserializer .class)
  private int age;
}

class MyIntDeserializer extends JsonDeserializer<Integer> {

  @Override
  public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    String text = p.getText();
    if (text == null || text.equals("")) {
      return 0;
    }

    int result;
    try {
      result = Integer.parseInt(text);
    } catch (Exception ex) {
      throw new RuntimeException("age must be number");
    }

    if (result < 0 || result >= 200) {
      throw new RuntimeException("age must in (0, 200)");
    }

    return result;
  }
}

Expected output:

{
  "code": 400,
  "message": "JSON parse error: age must in (0, 200); nested exception is com.fasterxml.jackson.databind.JsonMappingException: age must in (0, 200) (through reference chain: com.example.demo.validation.Person[\"age\"])",
  "data": null
}
Wargo answered 28/8, 2022 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.