Search code examples
javaspringvalidationspring-validator

How to validate a Map<String, String> using Spring Validator programmatically


I have a Map that I receive from a browser redirection back from a third party to my Spring Controller as below -

@RequestMapping(value = "/capture", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void capture(@RequestParam
    final Map<String, String> response)
    {
        // TODO : perform validations first.
        captureResponse(response);
    }

Before using this payload, I need to do non-trivial validation, involving first checking for non-null values of a map, and then using those values in a checksum validation. So, I would like to validate my payload programmatically using the Spring Validator interface. However, I could not find any validator example for validating a Map.

For validating a Java Object, I understand how a Validator is invoked by passing the object and a BeanPropertyBindingResult to contain the errors to the Validator as below -

final Errors errors = new BeanPropertyBindingResult(object, objectName);
myValidator.validate(object, errors);
if (errors.hasErrors())
{
    throw new MyWebserviceValidationException(errors);
}

For a Map, I can see that there is a MapBindingResult class that extends AbstractBindingResult. Should I simply use it, and pass my map in the Object object and in the validator cast it back to a Map? Also, how would the Validator method of supports(final Class<?> clazz) be implemented in my validator? Would it simply be like below code snippet where there can only be one validator supporting this generic class of HashMap? Somehow doesn't feel right. (Although this does not matter to me as I will be injecting my validator and use it directly and not through a validator registry, but still curious.)

@Override
public boolean supports(final Class<?> clazz)
{
    return HashMap.class.equals(clazz);
}

Since, there is a MapBindingResult, I'm positive that Spring must be supporting Maps for validation, would like to know how. So would like to know if this is the way to go, or am I heading in the wrong direction and there is a better way of doing this.

Please note I would like to do this programmatically and not via annotations.


Solution

  • Just like I thought, Spring Validator org.springframework.validation.Validator does support validation of a Map. I tried it out myself, and it works!

    I created a org.springframework.validation.MapBindingResult by passing in the map I need to validate and an identifier name for that map (for global/root-level error messages). This Errors object is passed in the validator, along with the map to be validated as shown in the snippet below.

    final Errors errors = new MapBindingResult(responseMap, "responseMap");
    myValidator.validate(responseMap, errors);
    if (errors.hasErrors())
    {
        throw new MyWebserviceValidationException(errors);
    }
    

    The MapBindingResult extends AbstractBindingResult and overrides the method getActualFieldValue to give it's own implementation to get field from a map being validated.

    private final Map<?, ?> target;
    
    @Override
    protected Object getActualFieldValue(String field) {
        return this.target.get(field);
    }
    

    So, inside the Validator I was able to use all the useful utility methods provided in org.springframework.validation.ValidationUtils just like we use in a standard object bean validator. For example -

    ValidationUtils.rejectIfEmpty(errors, "checksum", "field.required");
    

    where "checksum" is a key in my map. Ah, the beauty of inheritance! :)

    For the other non-trivial validations, I simply cast the Object to Map and wrote my custom validation code.

    So the validator looks something like -

    @Override
    public boolean supports(final Class<?> clazz)
    {
        return HashMap.class.equals(clazz);
    }
    @Override
    public void validate(final Object target, final Errors errors)
    {
        ValidationUtils.rejectIfEmpty(errors, "transactionId", "field.required");
        ValidationUtils.rejectIfEmpty(errors, "checksum", "field.required");
    
        final Map<String, String> response = (HashMap<String, String>) target;
        // do custom validations with the map's attributes
        // ....
        // if validation fails, reject the whole map - 
             errors.reject("response.map.invalid");
    }