Post Thumbnail

How to Validate JSON Request Body in Spring Boot

1. Overview

We sometimes encounter server errors caused by a user providing input that’s longer than the database column size or even a non-existent ENUM value. Do not trust user input is a popular cliché that, if implemented, will save a lot of time and resources down the line.

That is why, in this article, we will be looking at the request-validator library, which is able to compare the user input against a pre-defined set of rules and return errors if any.

2. Dependency Installation

In order for us to use request-validator, we need to add it to our project’s pom.xml:

Listing 2.1 pom.xml

1
2
3
4
5
<dependency>
	<groupId>com.smattme</groupId>
	<artifactId>request-validator</artifactId>
	<version>0.0.2</version>
</dependency>

The latest version of the dependency is available on Maven central.

3. Validating JSON Request Body

Given that we have a simple login endpoint that requires a valid email and password and as a good Engineer we want to ensure that the user sends both fields and the email is a valid one.

We can easily achieve this with the request-validator library. For the email input field, we want the user to provide a non-null field and a valid email address while for the password field, we just want the user to provide a non-null value:

Listing 3.1 LoginController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
public class LoginController {


    @PostMapping("/auth/login")
    public ResponseEntity<GenericResponse> login(@RequestBody LoginRequest request) {

        Map<String, String> rules = new HashMap<>();
        rules.put("email", "required|email");
        rules.put("password", "required");

        List<String> errors = RequestValidator.validate(request, rules);
        if (!errors.isEmpty()) {
            GenericResponse genericResponse = new GenericResponse();
            genericResponse.setStatus(false);
            genericResponse.setCode(HttpStatus.BAD_REQUEST.value());
            genericResponse.setErrors(errors);
            genericResponse.setMessage("Missing required parameter(s)");
            return ResponseEntity.badRequest().body(genericResponse);
        }

        //otherwise all is well, process the request
        //loginService.login()

        return ResponseEntity.ok(GenericResponse.generic200ResponseObj("Login successful"));

    }
}

From Listing 3.1 above, we used a Map<String, String> to store the rules for each expected request field. The key of the map is the name of the field as the API user should supply it, while the value contains the validation rules.

We then call the RequestValidator.validate() method to check the incoming request object against the defined rules. The method returns a List<String> that’ll contain all the error messages if there are violations.

One big advantage of the library is that it returns a separate descriptive error message for each rule and checks ALL the rules in a single invocation. Because RequestValidator.validate() expects Object data type, the request object can be a Map, a POJO or even a JSON String.

If an API user provides an invalid request body, they’ll get a 400 Bad Request with a detailed list of all the data infractions:

Listing 3.2 Curl Request/Response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Request:

curl --location --request POST 'http://localhost:8080/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "john"
}'

Response:
{
    "status": false,
    "message": "Missing required parameter(s)",
    "errors": [
        "password is required",
        "email supplied is invalid"
    ],
    "code": 400
}

However, as expected, a valid request body will return success:

Listing 3.3 Curl Request/Response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Request:

curl --location --request POST 'http://localhost:8080/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "[email protected]",
    "password": "changeit"
}'


Response:

{
    "status": true,
    "message": "Login successful",
    "code": 200
}

Note that the returned List<String> of errors can be used as part of your response in any way you and your team have chosen. It’s not limited to the response format demonstrated in this article.

The complete source code is available at the end of this article and will help you better understand how the response format in this tutorial is formatted.

The request validator library allows us to combine one or more rules together using the pipe (|) character as a separator. From Listing 3.1 above, we combined the required and email rules together using the |.

The only exception to this is when using the regex rule with other rules. It should be the last rule and should be separated by double pipe characters as in ||. This is to accommodate the potential presence of the pipe character in the regex pattern.

Listing 3.3 Regex Pattern

1
2
Map<String, String> rules = new HashMap<>();
rules.put("dob", "required||regex:[0-9]{2}-[0-9]{2}-[0-9]{4}");

The complete list of rules is available here.

4. Defining Custom Rules

Let’s say we want to add a custom validation rule that’s not in the library by default, we can easily achieve it by subclassing the RequestValidator class and implementing the RuleValidator interface.

Given that we need to add a rule to ensure the user-provided value starts with custom_, first, we will need to create a PrefixRuleValidator class that’ll implement the RuleValidator interface and perform the custom logic:

Listing 4.1 PrefixRuleValidator.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class PrefixRuleValidator implements RuleValidator {
    private static final String CUSTOM_PREFIX = "custom_";
    @Override
    public ValidationResult isValid(Object value, Rule rule) {
        return value != null && String.class.isAssignableFrom(value.getClass()) &&
                value.toString().startsWith(CUSTOM_PREFIX)
                ? ValidationResult.success()
                : ValidationResult.failed(rule.getKey() + " should start with " + CUSTOM_PREFIX);
    }
}

The next component we need is a class that will extend RequestValidator. We will be calling this CustomRequestValidator, instead of the library’s RequestValidator, to do our checks:

Listing 4.2 CustomRequestValidator.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class CustomRequestValidator extends RequestValidator {

    static {
        ruleValidatorMap.put("customprefix", PrefixRuleValidator.class);
    }

    public static List<String> validate(Object target, Map<String, String> rules) {
        String jsonRequest = convertObjectRequestToJsonString(target);
        return validate(jsonRequest, rules, ruleValidatorMap);
    }


}

The structure of CustomRequestValidator is very simple, we statically added the PrefixRuleValidator class to the parent’s ruleValidatorMap. We then proceed to create a copy of the parent’s validate() method, this will effectively make our rules to be available alongside other default rules.

Finally, let’s use our custom rule in a controller:

Listing 4.3 CustomPrefixController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class CustomPrefixController {

    @PostMapping("/custom")
    public ResponseEntity<GenericResponse> formCustomPrefix(@RequestBody Map<String, Object> request) {

        Map<String, String> rules = Collections.singletonMap("objectType", "customprefix");

        List<String> errors = CustomRequestValidator.validate(request, rules);
        if(!errors.isEmpty()) {
            GenericResponse genericResponse = new GenericResponse();
            genericResponse.setStatus(false);
            genericResponse.setCode(HttpStatus.BAD_REQUEST.value());
            genericResponse.setErrors(errors);
            genericResponse.setMessage("Missing required parameter(s)");
            return ResponseEntity.badRequest().body(genericResponse);
        }

        return ResponseEntity.ok(GenericResponse.generic200ResponseObj("Operation successful"));
    }
}

Posting a valid request will return 200 OK:

Listing 4.4 Curl Request/Response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Request:

curl --location --request POST 'http://localhost:8080/custom' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectType": "custom_john"
}'

Response:

{
    "status": true,
    "message": "Operation successful",
    "code": 200
}

On the other hand, posting an invalid request will return the error message as coded in Listing 4.1 PrefixRuleValidator.java:

Listing 4.5 Curl Request/Response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Request:

curl --location --request POST 'http://localhost:8080/custom' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectType": "john"
}'

Response:

{
    "status": false,
    "message": "Missing required parameter(s)",
    "errors": [
        "objectType should start with custom_"
    ],
    "code": 400
}

5. Conclusion

In this article, we’ve seen how we can easily validate the JSON request body and ensure the API consumers are sending the data we expect alongside practical examples. The complete source code is available on GitHub.

Happy coding

Seun Matt

Results-driven Engineer, dedicated to building elite teams that consistently achieve business objectives and drive profitability. With over 9 years of experience, spannning different facets of the FinTech space; including digital lending, consumer payment, collections and payment gateway using Java/Spring Boot technologies, PHP and Ruby on Rails