loading...
Cover image for Dead simple REST API validation with Spring Boot (seriously, very simple)

Dead simple REST API validation with Spring Boot (seriously, very simple)

baso53 profile image Sebastijan Grabar ・7 min read

This post is written in a pretty leisurely fashion

REST API validation... I seriously think that people don't really like to do it, or they just do it sloppily. In other words, people don't care about validation as much as they should. The end result is not really that catastrophic, at least in the Spring Framework ecosystem. Usually, some method deeper in the call stack will throw an Exception and Spring will take care about the rest of it. You will probably get a HTTP 4xx or 5xx status code as the response, with some (usually) undecipherable call stack. Who reads call stacks from HTTP responses anyway, we're not crazy. Someone might say it's not such a big deal, since there's a good chance that no harm has been done and end users won't notice it. Well, except if you show them all the HTTTP error messages, in which case, your mistake.

Oh hell. No.

Seriously, do your validation. Do it right. You'll be happier.

Spring can have some pretty scary and complicated validation logic when you first look at it. It doesn't have to be though.

In this post, I'll show you one way. The way I love to do it. We'll validate us some query parameters.

Meet Lombok

Chances are, if you're programming with Java, you are already using Lombok. Mighty fine. Lombok is insanely awesome and I am really glad that I have a chance to be using it.

If you don't know what Lombok does for you, here are some basic things that it can do:

  • generate all the getters and setters for your class fields
  • generate constructors with all/no parameters
  • generate a Builder for your class
  • generate hashCode() and toString() methods for your class
  • do all sort of data mapping with delegates

For purposes of this post, we'll only be using the @Data annotation, which will generate no args constructor and all the getters and setters.

Preparing your project

We're not going to delve into Spring basics here, what's Maven/Gradle and other unnecessary stuff. The only thing you need to do, if you want to follow this tutorial, is:

  1. Go to https://start.spring.io/
  2. Add Spring Web and Lombok as dependencies
  3. Select Java as the Language (if you haven't realised this post is about Java yet)

Validating query parameters in Spring controller

Okay, let's say that as an example, I want to make an endpoint that will fetch us some entities. What type of entities, we don't care at all at the moment, the only thing we want to do right now, is validate the parameters that are coming into the controller method.

If you're a little confused, the parameters I'm talking about are query strings, or query params, whatever you want to call them. Basically, they are a way to parameterize a URL.

Let's say we want 4 parameters coming into our controller method that we want to validate, with the following rules:

  1. name - must be provided
  2. timestamp - must also be provided and must be in correct format (ISO 8601)
  3. code1 - must be exactly 3 alphanumerical characters
  4. code2 - must be between 4 and 5 characters long

Yes, I know. I'm very imaginative with my naming. Bear with me.

An additional rule we want to have is that we have to provide either code1 or code2, or both of them. That's some more complicated logic right there.

So another rule:

  1. code1 or code2, or both, must be provided

Now I'm just going to show you the whole code for it.

package com.example.demo;

import lombok.Data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.Instant;

@RestController
public class DemoController {

    @GetMapping("query")
    public void queryParams(@Valid RequestParams params) {
        // your controller code here
    }

    @Data
    public static class RequestParams {

        @NotBlank
        private String name;

        @NotNull
        private Instant timestamp;

        @Pattern(regexp = "^[0-9A-Za-z]{3}$")
        private String code1;

        @Size(min = 4, max = 5)
        private String code2;

        @AssertTrue(message = "You must provide at least code1 or code2")
        private boolean isCode1orCode2Provided() {
            return code1 != null || code2 != null;
        }
    }
}

Let me explain, through steps:

  1. Create a new class that has all of your parameters. Note that it doesn't need to be named RequestParams, I just named it like that for this example. Spring will bind all the query params to your class - by default.
  2. Use Java Bean Validation API (JSR-380 specification, that's from javax.validation package) to put any constraint you want on the individual query params.
  3. Use @AssertTrue to handle any of the validation requirements that handle multiple query params. The method annotated with this annotation must return true for the whole request to be valid.
  4. Accept the new class in your controller method and put on a @Valid annotation. The annotation enables validation for this object, which is done by Spring. Spring will bind all the query params to your class - by default.
  5. Profit!

I'm not going to go through what each of the annotations does, you can find that out in the official spec (https://beanvalidation.org/2.0/spec/#builtinconstraints) or the matching JavaDoc (https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html). I hate JavaDoc, so I just do it through the IDE.

There's no more implementing custom validators, ugly controller method signatures, custom validation annotations and all the things that in the end just make you project more bloated. YAGNI!

Seriously, there are so many different annotations to choose from in the javax.validation package, that there's a good change they might just be all you need. Usually, you can validate almost every field with only these annotations and for everything else, you can use @AssertTrue. There's also an @AssertFalse annotation, if it suits you more to write the validation in the other way. You can also add a custom message for any of the annotation, if the default ones aren't enough (they usually are, though). Check it out yourself, experiment!

Let's test this now. We'll try to make a request that violates one of our validation rules, let's say, we don't send code1 and code2 and expect that we get an error message.

If we run the application and make a GET request with this URL:
http://localhost:8080/query?name=johnny&timestamp=2018-08-29T09:25:01Z

we'll get a HTTP 400 status code and the following response:

{
    "timestamp": "2020-05-05T16:21:09.450+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "AssertTrue.requestParams.code1orCode2Provided",
                "AssertTrue.code1orCode2Provided",
                "AssertTrue"
            ],
            "arguments": [
                {
                    "codes": [
                        "requestParams.code1orCode2Provided",
                        "code1orCode2Provided"
                    ],
                    "arguments": null,
                    "defaultMessage": "code1orCode2Provided",
                    "code": "code1orCode2Provided"
                }
            ],
            "defaultMessage": "You must provide at least code1 or code2",
            "objectName": "requestParams",
            "field": "code1orCode2Provided",
            "rejectedValue": false,
            "bindingFailure": false,
            "code": "AssertTrue"
        }
    ],
    "message": "Validation failed for object='requestParams'. Error count: 1",
    "path": "/query"
}

You might say that the response is too huge for just a validation error message, but why should that be a problem? More often than not, error messages are for developers, not the users. I'd rather have a bigger error message, than a smaller one. And from this one, you can find out a LOT.

The other way

The other way to do validation of query params is through the inline @RequestParam annotation. Like in the example below:

package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.Instant;

@RestController
public class DemoController {

    @GetMapping("query")
    public void queryParams(
            @RequestParam @NotBlank String name,
            @RequestParam @NotNull Instant timestamp,
            @RequestParam @Pattern(regexp = "^[0-9A-Za-z]{3}$") String code1,
            @RequestParam @Size(min = 4, max = 5) String code2
            ) {

        if (code1 == null && code2 == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You must provide at least code1 or code2");
        }

        // your controller code here
    }
}

Our method signature now looks kinda.... big. There's a lot going on, for sure.

I deliberately put some validation logic inside the controller code. Is that wrong? I don't know, maybe not, but it sure does leak validation logic into the place (I'm talking about the method's body) where other things should be happening. And now you also have two kinds of messages to expect from your REST API when there's a validation error. If you think about it, the interpreter should never reach the method body, if the query params aren't valid.

The "correct" way would be to implement a custom validation annotation and it's corresponding handler (a class that in some way implements the Validator interface).

If you do that, you already added two extra files. To validate one simple thing. The syntax to do that is also kind of, well, sad. Or, maybe, you just want your codebase to look more Enterprise. No thanks, I'll avoid that with a very big radius.

Notice that I put correct in quotes. I don't really think that there is an absolutely correct way. There are better ways and worse ways, more maintainable ways and less maintainable ways, prettier ways and uglier ways. It all depends on what you want.

Other uses, eg. validating the request body

Example from this post focused on validating query params. You can basically use the same API to validate the request body as well. The only thing you need to add is the @RequestBody annotation along with @Valid, so that Spring knows that it has to bind the request body to this object and not the query params.

Everything else remains the same.

That way, you can easily validate your incoming DTOs and you don't need to worry about complex validation logic. And, it's so easy to add new DTOs.

Conclusion

If you like doing things concisely and with less code, this way of validation will leave you satisfied. Spring Framework and the JSR-380 API works wonders. This way, you'll have a standardized validation logic across your whole application and standardized set of responses for the validation errors as well. And you have to do almost none of that yourself.

Granted, this approach has some downsides. For example, unit testing the validation logic is one problem. Should you test features of Spring Framework? Probably not! But, you might want to test the custom logic that you have in the methods annotated with @AssertTrue and @AssertFalse. There are ways around that, because you can externalize these methods with method references, but this whole "less code" story then falls into the water.

That argument stands only for unit testing. You can however, integration test your controller (so that the whole Spring behemoth starts) and only test it for the validation. Can that count as unit testing? Might as well, if you ask me.

Other downsides, I don't know. You tell me.

Posted on May 5 by:

baso53 profile

Sebastijan Grabar

@baso53

Web enthusiast and a problem solver.

Discussion

markdown guide