How to validate REST calls in Spring Boot

In our previous post, we discussed how to get started with REST in Spring boot, and how to get a REST endpoint up and running. However, callers of the endpoint were able to send any data to our server, and this data would be accepted and saved by our server without any sort of checks or validation.

In this post, we will discuss how to validate data sent by requesters during REST calls to a REST-endpoint in Spring boot using Java Bean Validation.

What is Java Bean Validation

Java Bean Validation, AKA JSR-303 is a Java specification for an API which can be used by developers to specify a set of constraints on their application data. These constraints can then be applied to make sure that incoming data is valid. In other words, you can use Java Bean Validation to make sure that the incoming data fits certain rules and conditions.

Examples of these constraints can be that an object field is not null, a String method parameter has a minimum length, etc.

        public void argumentNotNull( @NotNull String something){
            //do something
        }

While bean validation can be used at any level of your application, it is particularly useful when building a REST interface as we will see later.

It is not in the scope of this post to go into great detail in bean validation. However, we would like to introduce some commonly used validation constraints for those who are not familiar with them. If you are already familiar with bean validation, then you can skip ahead to the next section.

@NotNull

The @NotNull constraint, as the name implies, validates that the given field does not have a null value.

@Min

The @Min constraint is used to imply a minimum of something, depending on the field which is annotated with this constraint. This can be:

  • A minimum value of a numerical field. For example, integers, bytes, shorts, longs and their respective wrappers.
  • A minimum value of a BigDecimal.
  • A minimum value of a BigInteger.

Note that doubles and floats are not supported due to potential rounding errors. Also note that null values are automatically considered valid.

@Size

The @Size constraint is a very versatile one as it can be added to collections and arrays. This constraint ensures that the given array/collection have at least the given stated number of elements.

  • A minimum number of elements, if the annotation is added to a collection field (lists, sets).
  • A minimum length if added to a String, CharSequence or an array.
  • A minimum number of elements if added to a Map.

There are a lot more constraints that could be used for validation. However, we should now go back to our main focus of this post, which is to validate incoming REST requests.

Defining constraints on the REST models

As mentioned in our previous tutorial, it is a very good idea to have dedicated REST models to use as DTOs for our REST endpoints. This makes these models independent from any other inner application models and allows us to add the constraints that are needed, without worrying about breaking any compatibilities.

In the previous post, we created the “TicketRestModel”. However, we did not define any constraints for the inner fields. Let us start by adding NotNull constraints to the “title” and “description” fields. We will also add constraints to make sure that these fields have minimum lengths of 10 and 20 characters respectively.

package com.nullbeans.customerservice.incidentmanagement.rest.models;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
@NoArgsConstructor
public class TicketRestModel {

    public long identifier;

    @NotNull
    @Size(min = 10)
    public String title;

    @NotNull
    @Size(min = 20)
    public String description;
    
    public String status;

}

The next step we need to do is to activate these constraints as we will see in the next section.

Activating validation constraints

Luckily, it is quite easy in Spring boot to activate validation constraints. All you need to do is to add the @Valid annotation next to the method parameter to be validated.

Let us revisit the TicketController class and mark the “ticketInput” parameter with the @Valid annotation.

    @PostMapping
    public ResponseEntity<TicketRestModel> addTicket(@RequestBody @Valid TicketRestModel ticketInput) throws URISyntaxException {

        Ticket ticket = new Ticket();
        ticket.setStatus("NEW");
        ticket.setTitle(ticketInput.getTitle());
        ticket.setDescription(ticketInput.getDescription());

        ticketRepository.save(ticket);

        ticketInput.setIdentifier(ticket.getId());
        ticketInput.setStatus(ticket.getStatus());

        return ResponseEntity.created(new URI(String.valueOf(ticket.getId()))).body(ticketInput);
    }

Now let us test the REST endpoint, by sending a POST request with the following body.

{
"description":"I would like to learn REST in Spring"
}

Doing this will result in an HTTP 400 error (bad request) being returned by the server. The server will also send along the following report, indicating that the “title” field is null.

{
   "timestamp": "2020-03-29T17:10:34.405+0000",
   "status": 400,
   "error": "Bad Request",
   "errors": [   {
      "codes":       [
         "NotNull.ticketRestModel.title",
         "NotNull.title",
         "NotNull.java.lang.String",
         "NotNull"
      ],
      "arguments": [      {
         "codes":          [
            "ticketRestModel.title",
            "title"
         ],
         "arguments": null,
         "defaultMessage": "title",
         "code": "title"
      }],
      "defaultMessage": "must not be null",
      "objectName": "ticketRestModel",
      "field": "title",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
   }],
   "message": "Validation failed for object='ticketRestModel'. Error count: 1",
   "path": "/api/tickets"
}

Now, let us try the endpoint one more time. This time, we will send the title, but with less than 10 characters.

{
"title":"abc123",
"description":"I would like to learn REST in Spring"
}

Doing so will also result in an HTTP 400 error with the following contents.

{
   "timestamp": "2020-03-29T17:15:59.131+0000",
   "status": 400,
   "error": "Bad Request",
   "errors": [   {
      "codes":       [
         "Size.ticketRestModel.title",
         "Size.title",
         "Size.java.lang.String",
         "Size"
      ],
      "arguments":       [
                  {
            "codes":             [
               "ticketRestModel.title",
               "title"
            ],
            "arguments": null,
            "defaultMessage": "title",
            "code": "title"
         },
         2147483647,
         10
      ],
      "defaultMessage": "size must be between 10 and 2147483647",
      "objectName": "ticketRestModel",
      "field": "title",
      "rejectedValue": "abc123",
      "bindingFailure": false,
      "code": "Size"
   }],
   "message": "Validation failed for object='ticketRestModel'. Error count: 1",
   "path": "/api/tickets"
}

Notice this time that the server sent a message that the size must be between 10 and 2147483647 (max array size).

Please keep in mind that the validation has been activated only for the addTicket method. All other methods remain unaffected as we did not add the @Valid annotation to them.

Validation Groups and why we need them

Now, let us discuss the possibility that we would like to activate validation for the updateTicket method as well. But this time, we need to make sure that the REST endpoint caller not only sends the “title” and “description fields”, but the status field as well.

We can add the @NotNull annotation to the status field in the TicketRestModel. However, this will cause a conflict with the addTicket method because in that use case, we only need to validate the “title” and “desciption” fields.

    @NotNull
    public String status;

Now, if we make a POST call to add a new ticket with the following contents, we will get an error.

{
"title":"My brand new ticket",
"description":"I would like to learn REST in Spring"
}

The server now complains that the status field is missing.

.......
         ],
         "arguments": null,
         "defaultMessage": "status",
         "code": "status"
      }],
      "defaultMessage": "must not be null",
      "objectName": "ticketRestModel",
      "field": "status",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
   }],
   "message": "Validation failed for object='ticketRestModel'. Error count: 1",
   "path": "/api/tickets"
......

So how can we activate different sets of constraints for different circumstances? Here, validation groups come to the rescue.

Validation groups are markers that are used to mark a certain set of constrains and group them into different constraint groups. This causes these constraints to be activated only if the specific validation group is activated.

Please also note that once you mention that validation should occur for a specific validation group, then constraints which do not belong to this group are not activated.

To understand the mechanism of how validation groups work, let us try them out by creating a validation group and call it “OnUpdate”. All you need to do is to create an empty class or an interface.

//Validation group to be activated when updating an element
public interface OnUpdate {
}

Now, let us mark the NotNull constraint on the status field with the OnUpdate group.

    @NotNull(groups = {OnUpdate.class})
    public String status;

This will cause the constraint to be activated only when the OnUpdate group is activated.

Let us try out the addTicket method by resending the following content.

{
"title":"My brand new new ticket",
"description":"I would like to learn REST in Spring"
}

And the result will be a successful creation of a ticket.

HTTP/1.1 201 
Location: 17
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 29 Mar 2020 17:56:48 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{"identifier":17,"title":"My brand new new ticket","description":"I would like to learn REST in Spring","status":"NEW"}

This is because the validation group OnUpdate is not activated in the addTicket method.

Activating validation groups

To activate validation groups, we will need to use the @Validated annotation from Spring. This will replace the @Valid annotation from the standard bean validation specification. Let us activate the validation group by adding the annotation to the updateTicket method in the TicketController.

    @PutMapping("/{ticketId}")
    public ResponseEntity<Void> updateTicket(@PathVariable long ticketId, @RequestBody @Validated(OnUpdate.class) TicketRestModel ticketInput){

        Ticket ticket = ticketRepository.findById(ticketId).get();
        ticket.setStatus(ticketInput.getStatus());
        ticket.setTitle(ticketInput.getTitle());
        ticket.setDescription(ticketInput.getDescription());

        ticketRepository.save(ticket);
        return ResponseEntity.ok().build();
    }

Now, let us send an PUT request, without the status field.

{
"title":"my updated title",
"description":"I would like to learn REST in Spring"
}

We will get the following response.

{
   "timestamp": "2020-03-29T18:03:45.820+0000",
   "status": 400,
   "error": "Bad Request",
   "errors": [   {
      "codes":       [
         "NotNull.ticketRestModel.status",
         "NotNull.status",
         "NotNull.java.lang.String",
         "NotNull"
      ],
      "arguments": [      {
         "codes":          [
            "ticketRestModel.status",
            "status"
         ],
         "arguments": null,
         "defaultMessage": "status",
         "code": "status"
      }],
      "defaultMessage": "must not be null",
      "objectName": "ticketRestModel",
      "field": "status",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
   }],
   "message": "Validation failed for object='ticketRestModel'. Error count: 1",
   "path": "/api/tickets/12"

While this approach has solved one problem for us, it has created another. This is because only the constraints marked by the validation groups mentioned in the @Validated annotation are activated. This means that any other constraints are inactive.

For example, if we send a PUT request with a missing “title” field, it will be accepted.

{
"status":"IN_PROGRESS",
"description":"No title is added in this request"
}
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 29 Mar 2020 18:06:36 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Therefore, one has to be careful with validation groups. For a particular use-case, you will need to mark all the constraints that you need to activate for that particular use case in the active validation group (or groups).

For example, in the update use-case, we need to validate the “title”, “description” and “status” of the ticket. Therefore, we will add these constraints to the OnUpdate group. We will also need to create a new group called OnCreate in order to activate validation for the “title” and “description” fields in the creation use-case (otherwise, validation for these fields will no longer be triggered).

To create the OnCreate group, we again create an empty interface.

package com.nullbeans.customerservice.incidentmanagement.rest.validationgroups;

/**
 * This validation group will be activated when a new item is being added.
 */
public interface OnCreate {
}

Finally, we will mark the validation constraints in the TicketRestModel class with the appropriate validation groups.

package com.nullbeans.customerservice.incidentmanagement.rest.models;

import com.nullbeans.customerservice.incidentmanagement.rest.validationgroups.OnCreate;
import com.nullbeans.customerservice.incidentmanagement.rest.validationgroups.OnUpdate;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
@NoArgsConstructor
public class TicketRestModel {

    public long identifier;

    @NotNull(groups = {OnCreate.class, OnUpdate.class})
    @Size(min = 10, groups = {OnCreate.class, OnUpdate.class})
    public String title;

    @NotNull(groups = {OnCreate.class, OnUpdate.class})
    @Size(min = 20, groups = {OnCreate.class, OnUpdate.class})
    public String description;

    @NotNull(groups = {OnUpdate.class})
    public String status;

}

Finally, let us try to resend the last request (a PUT request with a missing title). We will get the following response.

.......
      "defaultMessage": "must not be null",
      "objectName": "ticketRestModel",
      "field": "title",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
   }],
   "message": "Validation failed for object='ticketRestModel'. Error count: 1",
   "path": "/api/tickets/12"

Do not forget to activate the OnCreate validation group in the addTicket method.

    @PostMapping
    public ResponseEntity<TicketRestModel> addTicket(@RequestBody  @Validated(OnCreate.class) TicketRestModel ticketInput) throws URISyntaxException {

        Ticket ticket = new Ticket();
 ........
........

      

Summary and further info

In this post, we discussed how to validate data sent during REST calls. Before we end this post, we would like to mention a few more important points:

  • Using Java Bean Validation is not the only way to do validation. However, I found it to be the most efficient and convenient way.
  • Be careful when using validation groups. Our example was simple and hence there was not much complexity. However, things can get way too complex too fast if you rely on validation groups too much.
  • By default, Spring would send an error report to the caller along with an HTTP 400 error code. However, we can issue a more user friendly error report to the caller. This will be done during error handling in a future post.
  • There are other errors that can happen as a result of invalid user input, such as wrong URIs and IDs. These cases need to be handled outside the scope of bean validation. We will also address these scenarios in future posts.