How to manually trigger Spring validation?
Asked Answered
E

2

35

The annotated Spring validation on fields of a POJO works when it is created from JSON request body. However, when I create the same object manually (using setters) and want to trigger validation, I'm not sure how to do that.

Here is the Registration class, which has Builder inner class that can build the object. In the build() method I would like to trigger Spring validation. Please scroll to the bottom and check Builder.build() and Builder.validate() methods to see current implementation. I'm using javax.validation.Validator to trigger validation, but I prefer to leverage Spring validation if possible.

package com.projcore.dao;

import com.projcore.util.ToString;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Size;
import java.util.List;
import java.util.Set;

/**
 * The data transfer object that contains the information of a Registration
 * and validation rules for attributes.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public final class Registration {

    private static final Logger LOGGER = LoggerFactory.getLogger(Registration.class);

    private String id;

    @NotEmpty
    @Size(max = 255)
    private String messageId;

    @NotEmpty
    @Size(max = 255)
    private String version;

    @Size(max = 255)
    private String system;

    public Registration() {
    }

    private Registration(Builder builder) {
        this.id = builder.id;
        this.messageId = builder.messageId;
        this.version = builder.version;
        this.system = builder.system;
    }

    public static Builder getBuilder() {
        return new Builder();
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getSystem() {
        return system;
    }

    public void setSystem(String system) {
        this.system = system;
    }

    @Override
    public String toString() {
        return ToString.create(this);
    }

    /**
     * Builder pattern makes the object easier to construct in one line.
     */
    public static class Builder {

        private String id;

        private String messageId;

        private String version;

        private String system;

        private Builder() {}

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder messageId(String messageId) {
            this.messageId = messageId;
            return this;
        }


        public Builder version(String version) {
            this.version = version;
            return this;
        }

        public Builder system(String system) {
            this.system = system;
            return this;
        }

        public Registration build() {
            Registration entry = new Registration(this);
        
            // *** Would like to trigger Spring validation here ***
            Set violations = validate(entry);
            if (violations.isEmpty())
                return entry;
            else
                throw new RuntimeException(violations.toString());
        }
    
        private Set validate(Registration entry) {
            Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
            Set<ConstraintViolation<Registration>> constraintViolations = validator.validate(entry);
            return constraintViolations;
        }
        
    }
}

Validation works fine here:

@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
Registration create(@RequestBody @Valid Registration registration) 

Solution:

Removed Registraion.Builder.validate(). Updated Registraion.Builder.build() to:

public Registration build() {
    Registration entry = new Registration(this);
    return (Registration) ValidatorUtil.validate(entry);
}

ValidatorUtil.java

package projcore.util;

import com.ericsson.admcore.error.InvalidDataException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

public class ValidatorUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(ValidatorUtil.class);
    private static final Validator javaxValidator = Validation.buildDefaultValidatorFactory().getValidator();
    private static final SpringValidatorAdapter validator = new SpringValidatorAdapter(javaxValidator);

    public static Object validate(Object entry) {
        Errors errors = new BeanPropertyBindingResult(entry, entry.getClass().getName());
        validator.validate(entry, errors);
        if (errors == null || errors.getAllErrors().isEmpty())
            return entry;
        else {
            LOGGER.error(errors.toString());
            throw new InvalidDataException(errors.getAllErrors().toString(), errors);
        }
    }
}

InvalidDataException.java

package projcore.error;

import org.springframework.validation.Errors;

/**
 * This exception is thrown when the dao has invalid data.
 */
public class InvalidDataException extends RuntimeException {

    private Errors errors;

    public InvalidDataException(String msg, Errors errors) {
        super(msg);
        setErrors(errors);
    }

    public Errors getErrors() {
        return errors;
    }

    public void setErrors(Errors errors) {
        this.errors = errors;
    }
}
Expendable answered 24/2, 2015 at 17:51 Comment(0)
T
30

Spring provides full support for the JSR-303 Bean Validation API. This includes convenient support for bootstrapping a JSR-303 implementation as a Spring bean. This allows a javax.validation.Validator to be injected wherever validation is needed in your application.

Use the LocalValidatorFactoryBean to configure a default JSR-303 Validator as a Spring bean:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
        

The basic configuration above will trigger JSR-303 to initialize using its default bootstrap mechanism. A JSR-303 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.

5.7.2.1 Injecting a Validator

LocalValidatorFactoryBean implements both javax.validation.Validator and org.springframework.validation.Validator. You may inject a reference to one of these two interfaces into beans that need to invoke validation logic.

Inject a reference to javax.validation.Validator if you prefer to work with the JSR-303 API directly:

// JSR-303 Validator
import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

}
            

Inject a reference to org.springframework.validation.Validator if your bean requires the Spring Validation API:

// Spring Validator
import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

}

Here is a well exaplained example:
Using JSR 303 with "classic" Spring Validators (enter the SpringValidatorAdapter)

This link is very helpful. Wrapping javax.validation.Validator in org.springframework.validation.beanvalidation.SpringValidatorAdapter helped deal with errors consistently. Can you add this as an answer so that I can accept it

and Spring doc here.

Terricolous answered 24/2, 2015 at 18:59 Comment(6)
I think that Spring Boot also provides that Validator bean for you (not that it's hard to do yourself).Saxony
Does this mean spring validator can be used only in a @Service class to trigger validation? I have done enough reading online, so have seen examples like this, but I still don't understand how I can do it within the POJO, hence the question. I also know how to trigger validation for the custom validator, but not how to trigger default validation within a POJO. If possible, can you provide specific answer within the context of what I have.Expendable
You can inject the Validator to any spring bean. there is an old example here nonrepeatable.blogspot.com/2010/04/… . comment here if you still have concernsTerricolous
This link is very helpful. Wrapping javax.validation.Validator in org.springframework.validation.beanvalidation.SpringValidatorAdapter helped deal with errors consistently. Can you add this as an answer so that I can accept it.Expendable
I have edited this answer to include the link, so you can just accept this, and am glad that I can helpTerricolous
Ok I have a validator, I can validate. But what should I do with validation result? How can I convert it to some sort of exception that I can throw to end up with exactly the same result as when spring doing validation through @Validated annotation on controller's parameter? I understand that I can do this manually, manually serialize to json etc, but shouldn't it be easier way? Just way to mimic a behaviour of validation through annotation, but programmatically.Composition
H
1

To make sure it behaves like validation on fields of a POJO (exception, messages) you can use DataBinder

import org.springframework.validation.Validator;

@RestController
class Controller {

  @Autowired
  private Validator validator;

  @RequestMapping(method = RequestMethod.POST)
  @ResponseStatus(HttpStatus.CREATED)
  Registration create() {
    Registration registration = Registraction.getBuilder()....build();
    DataBinder binder = new DataBinder(registration);
    binder.setValidator(validator);
    binder.validate();
    if (binder.getBindingResult().hasErrors()) {
      Method method = this.getClass().getMethod("create");
      MethodParameter methodParameter = new MethodParameter(method, 0);
      throw new MethodArgumentNotValidException(methodParameter, binder.getBindingResult());
    }
    return someService.create(registration);
  }

}
Hydria answered 28/3, 2023 at 5:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.