Search code examples
javajax-rsbean-validationwebsphere-liberty

Is there a way to get a variable in to a custom ConstraintValidator when using JAX-RS?


When de-serializing a JSON payload i want to validate a phone number using a custom validator. This validator needs to know what country the customer is in to correctly parse the phone number. The country i can find in the http header Accept-Language.

The problem is how can i provide the custom validator with this extra meta data?

I was sure that @Context would inject the HttpHeaders into the PhoneNumberValidator but this does not work:

    @Context
    private HttpHeaders mHttpHeaders;

The field is null.

API end point:

@Path("customer")
public class CustomerResource {

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response postCustomer(@Valid Customer pCustomer) {
        return Response.ok(pCustomer).build();
    }
}

POJO:

@Getter
@Setter
public class Customer {
    @NotNull
    private String firstName;

    @ValidPhoneNumber
    private String mobileNumber;
}

Validator interface:

@Documented
@Retention(RUNTIME)
@Target({ TYPE, FIELD })
@Constraint(validatedBy = { PhoneNumberValidator.class })
public @interface ValidPhoneNumber {
    String message() default "Phone number is not valid!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Custom validator:

@Slf4j
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {    
    @Override
    public boolean isValid(String vTelephone, ConstraintValidatorContext pContext) {
        final PhoneNumberUtil vPhoneNumberUtil = PhoneNumberUtil.getInstance();
        // TODO: Get the country in here somehow
        final String vCountry = "GB";      
        try {
            PhoneNumber vPhoneNumber = vPhoneNumberUtil.parse(vTelephone, vCountry);
            if (!vPhoneNumberUtil.isValidNumber(vPhoneNumber)) {
                log.error("Invalid {} phone number: {}", vCountry, vTelephone);
                return false;                
            }
        } catch (NumberParseException e) {
            log.error("Error parsing {} as a phone number in {}: {}", vTelephone, vCountry, e.getMessage());
            return false;
        }
        return true;
    }
}


Solution

  • The HttpHeaders object is not (yet) a CDI managed bean - I say yet because it is part of the plan for Jakarta's JAX-RS 3.0 to be a CDI bean, but for JAX-RS 2.1 and earlier, it is not.

    But it is possible for the resource class to be a CDI managed bean. So you could inject the HttpHeaders into the CustomerResource class, and then provide a getter method that would access the headers. Then use @Inject in your validator class to inject your resource. Then you can access the headers from the validator.

    Something like this:

    @RequestScoped
    public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
        @Inject
        CustomerResource customerResource;
    
        @Override
        public boolean isValid(String vTelephone, ConstraintValidatorContext pContext) {
            // Get the country in here using the original resource instance
            final String vCountry = customerResource.getHeaderString("COUNTRY");
            try {
                validate(vTelephone, vCountry);
            } catch (Throwable t) {
                System.out.printf("Error parsing {0} as a phone number in {1}: {2}", vTelephone, vCountry, t.getMessage());
                return false;
            }
            return true;
        }
    ...
    

    Updating the resource class like this:

    @RequestScoped
    @Path("customer")
    public class CustomerResource {
    
        @Context
        private HttpHeaders headers;
    
        @POST
        @Consumes(MediaType.APPLICATION_JSON)
        public Response postCustomer(@Valid Customer pCustomer) {
            return Response.ok(pCustomer).build();
        }
    
        String getHeaderString(String headerName) {
            return headers.getHeaderString(headerName);
        }
    ...
    

    I wrote up a working sample here that may help: https://github.com/andymc12/ContextVarInValidatorSample

    Hope this helps, Andy