11

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.

Nobita
  • 598
  • 3
  • 6
  • 16
  • 1
    How could a boolean possibly have another value? A Boolean is not a string. It doesn't contain characters. – JB Nizet May 30 '19 at 15:30
  • `Pattern` annotation checks a `CharSequence` not a `Boolean` Object according to the documentation: https://docs.oracle.com/javaee/7/api/javax/validation/constraints/Pattern.html 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 already – pandaadb May 30 '19 at 15:33
  • @JBNizet But it's accepting "true" as a string. – Nobita May 30 '19 at 15:54
  • 1
    @Nobita 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. – JB Nizet May 30 '19 at 15:57
  • @JBNizet How can I configure Json parser to handle this? I’m using Jackson ObjectMapper class. – Nobita May 30 '19 at 19:05
  • By writing a custom deserializer for booleans, probably. – JB Nizet May 31 '19 at 06:31

6 Answers6

16

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.

Technoshaft
  • 679
  • 6
  • 18
  • 1
    But this will restrict the field to only have true. Right? – Nobita May 30 '19 at 15:42
  • 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. – Technoshaft May 30 '19 at 15:49
  • I tried but it allows "true" as a string, which I want to restrict. – Nobita May 30 '19 at 15:52
  • 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. – Technoshaft May 30 '19 at 15:54
  • 1
    Not what the OP was asking for – Stefan Haberl Jan 24 '22 at 08:34
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);
    }
Logan Smith
  • 139
  • 1
  • 8
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.

Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • 1
    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. – CasualViking May 30 '19 at 15:37
  • 3
    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. – pandaadb May 30 '19 at 15:38
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.

jaden kong
  • 11
  • 2
  • Please provide additional details in your answer. As it's currently written, it's hard to understand your solution. – Community Sep 01 '21 at 05:23
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);
    }
}
axel4u
  • 71
  • 1
  • 4
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
}
ikhvjs
  • 5,316
  • 2
  • 13
  • 36