How to Implement Validation for RESTful Services with Spring

Spring Bean Validation

Data validation is not a new topic in web application development.

We take a brief look at data validation in the Java ecosystem in general and the Spring Framework specifically. The Java platform has been the de facto standard for implementing data validation that is Bean Validation specification. Bean Validation specification has several versions:

  • 1.0 (JSR-303),
  • 1.1 (JSR-349),
  • 2.0 (JSR 380) – the latest version
guide on how to implement validation for restful services with spring

This specification defines a set of components, interfaces, and annotations. This provides a standard way to put constraints to the parameters and return values of methods and parameters of constructors, provide API to validate objects and object graphs.

A Declarative model is used to put constraints in the form of annotations on objects and their fields. There are predefined annotations like @NotNull, @Digits, @Pattern, @Email, @CreditCard. There is an ability to create new custom constraints.

Validation can run manually or more naturally, when other specification and frameworks validate data at the right time, for example, user input, insert or update in JPA.

Validation in Java Example

Let’s take a look at how it can be done in practice in this simple Bean Validation example inside regular Java application.

We have an object that we want to validate with all fields annotated with constraints.

public class SimpleDto {

  @Min(value = 1, message = "Id can't be less than 1 or bigger than 999999")
  @Max(999999)
  private int id;

  @Size(max = 100)
  private String name;

  @NotNull
  private Boolean active;

  @NotNull
  private Date createdDatetime;

  @Pattern(regexp = "^asc|desc$")
  private String order = "asc";

  @ValidCategory(categoryType="simpleDto")
  private String category;
  …
  Constructor, getters and setters

Note: @Min, @Max, @Size, @NotNull, and @Pattern are standard annotations, and @ValidCategory is custom.

Now we can use it in a simple Java application and manually validate the object.

public class SimpleApplication {

  public static void main(String[] args) {

    final SimpleDto simpleDto = new SimpleDto();
    simpleDto.setId(-1);
    simpleDto.setName("Test Name");
    simpleDto.setCategory("simple");
    simpleDto.setActive(true);
    simpleDto.setOrder("asc");
    simpleDto.setCreatedDatetime(new Date());

    ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    Validator validator = validatorFactory.usingContext().getValidator();

    Set constrains = validator.validate(simpleDto);
    for (ConstraintViolation constrain : constrains) {

       System.out.println(
      "[" + constrain.getPropertyPath() + "][" + constrain.getMessage() + "]"
      );

    }

  }

}

And result in Console will be:

“[id] [Id can't be less than 1 or bigger than 999999]”

Validation Constraint and Annotation

Create a custom validation constraint and annotation.


 @Retention(RUNTIME)
   @Target(FIELD)
   @Constraint(validatedBy = {ValidCategoryValidator.class})
   public @interface ValidCategory {

      String categoryType();

      String message() default "Category is not valid";

      Class<?>[] groups() default {};

      Class<? extends Payload>[] payload() default {};

    }

And constraint validation implementation:

public class ValidCategoryValidator implements ConstraintValidator<ValidCategory, String> {

    private static final Map<String, List> availableCategories;

    static {

      availableCategories = new HashMap<>();
      availableCategories.put("simpleDto", Arrays.asList("simple", "advanced"));

    }

    private String categoryType;

    @Override
    public void initialize(ValidCategory constraintAnnotation) {

      this.setCategoryType(constraintAnnotation.categoryType());

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {

      List categories = ValidCategoryValidator.availableCategories.get(categoryType);
      if (categories == null || categories.isEmpty()) {

         return false;

      }

      for (String category : categories) {

         if (category.equals(value)) {

             return true;

      }

    }

    return false;

  }

}

In the example above, available categories come from a simple hash map. In real application use cases, they can be retrieved from the database or any other services.

Please note that constraints and validations could be specified and performed not only on the field level, but also on an entire object.

When we need to validate various field dependencies, for example, start date, it can’t be after the end date.

The most widely used implementations of Bean Validation specifications are Hibernate Validator and Apache BVal.

Validation with Spring

The Spring framework provides several features for validation.

  • Support for Bean Validation API versions 1.0, 1.1 (JSR-303, JSR-349) was introduced in Spring Framework starting with version 3.
  • Spring has its own Validator interface that is very basic and can be set in specific DataBinder instance. This could be useful for implementing validation logic without annotations.

Bean Validation with Spring

Spring Boot provides validation started which can be included in the project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

This starter provides a version of Hibernate Validator compatible with the current Spring Boot.

Using Bean Validation, we could validate a request body, query parameters, variables within the path (e.g. / /simpledto/{id}), or any method or constructor parameters.

POST or PUT Requests

In POST or PUT requests, for example, we pass JSON payload, Spring automatically converts it into Java object and now we want to validate resulting object. Let’s use SimpleDto object from the 1st example:

@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {

    @Autowired
    private SimpleDtoService simpleDtoService;

    @RequestMapping(path = "", method = RequestMethod.POST, produces = 
    "application/json")
    public SimpleDto createSimpleDto(

        @Valid @RequestBody SimpleDto simpleDto) {

      SimpleDto result = simpleDtoService.save(simpleDto);

      return result;

    }

}

We just added @Valid annotation to the SimpleDto parameter annotated with @RequestBody. This will tell Spring to process validation before making an actual method call. In case validation fails, Spring will throw a MethodArgument NotValidException which, by default, will return a 400 (Bad Request) response.

Validating Path Variables

Validating Path Variables works a little differently. The problem is that now we have to add constraint annotations directly to method parameters instead of inside of objects.

To make this work there are 2 possible options:

Option 1: @Validated Annotation

Add @Validated annotation to the controller at the class level to evaluate constraint annotations on method parameters.

Option 2: Path Variable

Use an object that represents the path variable as seen in the example below:

@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {

    @Autowired
    private SimpleDtoService simpleDtoService;

    @RequestMapping(path = "/{simpleDtoId}", method = RequestMethod.GET, produces = 
    "application/json")
    public SimpleDto getSimpleDto(

      @Valid SimpleDtoIdParam simpleDtoIdParam) {

    SimpleDto result = simpleDtoService.findById(simpleDtoIdParam.getSimpleDtoId());

    if (result == null) {

      throw new NotFoundException();

    }

    return result;

  }

}

In this case, we have SimpleDtoIdParam class that contains the simpleDtoId field which will be validated against a standard or custom Bean constraint annotation. Path Variable name (/{simpleDtoId}) should be the same as the field name (so Spring will be able to find the setter for this field).

private static final long serialVersionUID = -8165488655725668928L;

    @Min(value = 1)
    @Max(999999)
    private int simpleDtoId;

    public int getSimpleDtoId() {
    return simpleDtoId;
    }

    public void setSimpleDtoId(int simpleDtoId) {
    this.simpleDtoId = simpleDtoId;
    }

}

In contrast to Request Body validation, Path Variable validation throws ConstraintViolationException instead of MethodArgumentNotValidException. Therefore, we will need to create a custom exception handler.

The Java Spring framework also allows validating parameters on a service level with @Validated annotation (class level) and @Valid (parameter level).

Considering Spring JPA is using Hibernate underneath, it supports Bean Validation for entity classes as well. Please note that it’s probably not a good idea in many cases rely on this level of validation since it means that all logic before was dealing with invalid objects.

Spring Validation Interface

Spring defines its own interface for validation Validator (org.springframework.validation.Validator). It can be set for a specific DataBinder instance and implement validation without annotations (non-declarative approach).

To implement this approach we would need to:

  1. Implement the Validator Interface
  2. Add Validator

Implement Validator Interface

Implement the Validator interface, for example lets work with our SimpleDto class:

@Component
public class SpringSimpleDtoValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
    return SimpleDto.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

      if (errors.getErrorCount() == 0) {

        SimpleDto param = (SimpleDto) target;
        Date now = new Date();
        if (param.getCreatedDatetime() == null) {

          errors.reject("100",

            "Create Date Time can't be null");

        } else if (now.before(param.getCreatedDatetime())) {

          errors.reject("101",

            "Create Date Time can't be after current date time");

        }

      }

    }

}

Check here if the created Datetime timestamp is in the future.

Add Validator

Add Validator implementation to DataBinder:

@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {

    @Autowired
    private SimpleDtoService simpleDtoService;

    @Autowired
    private SpringSimpleDtoValidator springSimpleDtoValidator;

    @InitBinder("simpleDto")
    public void initMerchantOnlyBinder(WebDataBinder binder) {
    binder.addValidators(springSimpleDtoValidator);
    }

    @RequestMapping(path = "", method = RequestMethod.POST, produces = 
    "application/json")
    public SimpleDto createSimpleDto(

      @Valid @RequestBody SimpleDto simpleDto) {

    SimpleDto result = simpleDtoService.save(simpleDto);
    return result;

  }

}

Now we have SimpleDto validated using constraint annotations and our custom Spring Validation implementation.

Conclusion

This article touched on Java Spring Validation with a particular focus on Java Bean Validation specification. This includes its implementation and how it’s supported in the Spring Framework Validation Interface.

For further reference, here is the GitHub repository with code examples presented in this article.

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/dev/222753.html

(0)
上一篇 2022年1月6日 17:44
下一篇 2022年1月6日 17:45

相关推荐

发表回复

登录后才能评论