Code Review Videos > Java > JSON Payload Validation in Spring Boot

JSON Payload Validation in Spring Boot

In this post, we’re going to explore how to validate an incoming JSON payload in a Spring Boot application.

I wanted to use this exercise as a way to learn both validation, and automated testing. So we’re going to see unit tests for the validation logic in isolation, and also how this comes together with the API controller actions.

It turns out there’s a bunch of stuff to cover in these two concepts, so let’s dive on in.

What We Will Cover In This Post

We’ll cover the following:

  1. Validation & Automated Testing Dependencies – We’ll add the necessary dependencies to our project to enable validation, and automated testing.
  2. Creating a Data Transfer Object (DTO) – We’ll define a DTO class that will represent the structure of the incoming JSON payload.
  3. Adding Validation Constraints – Using annotations, we’ll add assertions to the DTO to enforce our validation rules.
  4. Unit Testing the Validation – We’ll write unit tests to ensure that the validation logic works as expected.
  5. Automated Testing – Finally, we’ll look at how to write unit and integration tests to validate the entire workflow from the API endpoint to the validation process.

In my head / written down on paper, this seemed like a pretty simple set of steps. However, a couple of things turned out to be more involved than anticipated.

But that’s good, right?

Because it means we get to learn stuff!

Project Dependencies

I’ve created a brand new project for this post. For that I used the Spring Initializr, and that generated the following pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>uk.co.a6software</groupId>
    <artifactId>json-payload-validation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>json-payload-validation</name>
    <description>Learning how to validate simple JSON payloads</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>Code language: HTML, XML (xml)

Let’s quickly cover why each dependency is there.

spring-boot-starter-web is the core dependency for our Spring Boot web application. It includes all the necessary components to create a RESTful web service, including Tomcat as the embedded container and Spring MVC to handle incoming HTTP requests.

spring-boot-devtools provides additional development-time features, such as automatic restarts and live reloads when making changes. This helps speed up the development process by reducing the need to manually restart the application after every change.

The spring-boot-starter-test dependency is the testing library provided by Spring Boot. It includes various libraries and tools that help us write both unit and integration tests. Here are some key components it brings in:

  1. JUnit 5 – The primary testing framework for writing unit tests in Java.
  2. Mockito – A powerful mocking library to create mock objects for unit testing.
  3. Hamcrest – A library of matchers for writing clean and readable test assertions.
  4. Spring Test – Provides utilities for writing Spring-specific integration tests, including support for @WebMvcTest and @SpringBootTest.

But don’t just take my word for this. You can see exactly what dependencies you get when using spring-boot-starter-test (or the other dependencies here) by looking at the package on MVN Repository.

Lastly, spring-boot-starter-validation brings in hibernate-validator, which we will use to annotate our objects with validation rules (like @NotNull, etc), and also the validator functionality.

How Does Maven Know What Version To Use?

One thing that really confused me when I first looked at my pom.xml file was this: how on earth does Maven know which versions of my dependencies to bring in? If you check the pom.xml, you’ll notice that it often doesn’t specify a version number for any of the dependencies.

So, how does Maven figure it out?

Well, I believe it’s to do with the inclusion, right at the top of the pom.xml file, of this entry:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>Code language: HTML, XML (xml)

Again, well worth a look at this on MVN Repository – especially the “Managed Dependencies” section.

Instead of us having to manually specify each version, this effectively locks in a set of curated versions that work well together, so we don’t have to worry about choosing the right versions ourselves.

That’s really neat – if a little obscured from the curious newb such as myself.

We can explicitly specify our own versions, of course, but perhaps it makes sense to let Spring Boot figure out the compatibility matrix for us, unless we really need to get specific.

Creating A Data Transfer Object (DTO)

Creating a DTO for this exercise was nice and easy.

I kept things extremely simple, with just one field that would contain some text.

package uk.co.a6software.json_payload_validation.dto;

public class SimplePayloadDto {
    private String text;

    public SimplePayloadDto() {
    }

    public SimplePayloadDto(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}
Code language: Java (java)

There are a couple of points here.

The first is that I needed to include the ‘no args’ constructor (line 6), so that serialisation / deserialisation would behave properly. I actually didn’t realise this until I got later on in the implementation – even though I’m sure I learned this lesson already.

The second is that this class can be refactored – significantly – to become a record:

package uk.co.a6software.json_payload_validation.dto;

public record SimplePayloadDto(
    String text
) {
}Code language: Java (java)

And in doing so, this also solves the need for the ‘no args’ constructor entirely.

Adding Validation Constraints

The whole point of this exercise is to test how incoming JSON payloads can be validated when using Spring Boot.

So far I have the DTO, which the controller code will use to take the incoming JSON payload and transform into an object of type SimplePayloadDto.

We covered above that we have added dependencies to the project to bring in assertions and a validator.

Adding those assertions to the DTO code is really simple – we just need to annotate the fields with the conditions we require.

package uk.co.a6software.json_payload_validation.dto;

import jakarta.validation.constraints.NotNull;

public record SimplePayloadDto(
        @NotNull(message = "Text must be provided")
        String text
) {
}Code language: Java (java)

For complete clarity, this looks different if using a class:

package uk.co.a6software.json_payload_validation.dto;

import jakarta.validation.constraints.NotNull;

public class SimplePayloadDto {
    @NotNull(message = "Text must be provided")
    private String text;

    public SimplePayloadDto() {
    }

    public SimplePayloadDto(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

}Code language: Java (java)

Note the use of the import from jakarta.validation.constraint.

This confused me, as plenty of older code examples exist out there on the internet that will use javax.validation.

I’m not sure of the history of this, and haven’t dug into it.

Unit Testing Our Validation Logic

As above, the inclusion of spring-boot-starter-validation as a project dependency means we get access to both assertions and a validator.

I wanted to isolate the validation logic from any further complexity, and use unit tests to understand how this code works.

Let’s see a first pass at the code:

// src/test/java/uk/co/a6software/json_payload_validation/dto/SimplePayloadDtoTest.java

package uk.co.a6software.json_payload_validation.dto;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertTrue;

class SimplePayloadDtoTest {

    private Validator validator;

    @BeforeEach
    void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    void whenTextIsProvided_thenValidationPasses() {
        SimplePayloadDto payload = new SimplePayloadDto("Some text");

        Set<ConstraintViolation<SimplePayloadDto>> violations = validator.validate(payload);

        assertTrue(violations.isEmpty());
    }
}
Code language: Java (java)

OK, so quite a lot happening here.

I figured I’d start with the ‘happy path’ because dealing with validation failures (violations) would likely be harder.

A bunch of this was copy / paste from a variety of Google search results, so I won’t profess to be any kind of expert.

What I can see is that I would always use a ‘before each’ step (lines 20-24).

This is a useful construct available in most (all?) unit testing frameworks. This code block allows us to ‘stuff’ that will apply / be available to all tests in the file.

In this instance we need an instance of a validator, and in typical Java fashion we use a factory method to get that builder instance.

My First Unit Test

Then it was time to actually write a test.

The flow should be pretty simple:

  1. Create a valid instance of the DTO
  2. Pass that instance to the validator
  3. Assert that the validation process passes

Getting that into code was trickier:

    @Test
    void whenTextIsProvided_thenValidationPasses() {
        SimplePayloadDto payload = new SimplePayloadDto("Some text");

        Set<ConstraintViolation<SimplePayloadDto>> violations = validator.validate(payload);

        assertTrue(violations.isEmpty());
    }
Code language: Java (java)

OK, so first, remember the @Test annotation!

Yeah, the test framework won’t see your test if you miss this  😳

Next, naming the test. I had a much more simplistic name for this test, but ChatGPT (aka my mentor) wrote out this name and I preferred it. However, in reality the test name only really helps us humans, and I’m not 100% sure on the standard naming convention. Take this as a rough guess.

Then, on line 3, we get underway.

Creating the DTO is super easy – as it should be. This is a plain and simple object.

After this I knew I needed to validate… but how?

Unsure, so I moved on to the assertion. In there, I had:

    @Test
    void whenTextIsProvided_thenValidationPasses() {
        SimplePayloadDto payload = new SimplePayloadDto("Some text");

        assertTrue(true);
    }Code language: JavaScript (javascript)

And of course that passes. It would be bad if it didn’t.

But it’s not super useful. So I had to figure out how to actually validate.

Fortunately, intellisense / JetBrains autocomplete is pretty good. That gave me:

validator.validate(payload);Code language: Java (java)

From there, I’ve learned that if you ctrl + hover over a method name, you can see what the return type will be. You can even, with a bit of mouse delicacy, copy out the return type from the pop up:

Doing that process gives:

Set<ConstraintViolation<SimplePayloadDto>> violations = validator.validate(payload);Code language: Java (java)

And the variable name violations comes from the fact that the implementation has just told us we will get back a Set of ConstraintViolation objects.

Lastly, more intellisense / autocomplete magic gave the answer to what to assert against – albeit without the most intuitive method name:

The fun here being if the violations Set is empty then we must have not had any validation failures.

.isValid() would have been nicer…

This was enough to get a valid pass.

Testing The Unhappy Validation Path

Testing the happy path was the easier of the two possibilities.

Fortunately that laid a lot of the ground work to getting a unit test written for the unhappy path.

The unhappy path is simply the situation where the object is invalid. In our case that would be where there was no text.

Let’s see that code in full, then step through it:

    @Test
    void whenTextIsNull_thenValidationFails() {
        SimplePayloadDto payload = new SimplePayloadDto(null);

        Set<ConstraintViolation<SimplePayloadDto>> violations = validator.validate(payload);

        assertFalse(violations.isEmpty());
        assertTrue(violations
                .stream()
                .anyMatch(v -> v.getMessage().equals("Text must be provided")));
    }
Code language: Java (java)

To get underway, I had to create an invalid DTO. This meant I had to pass in a null as the argument for our text property (line 3).

The validation process was the same.

But straight away I knew I could invert the assertion and get to a passing test:

    @Test
    void whenTextIsNull_thenValidationFails() {
        SimplePayloadDto payload = new SimplePayloadDto(null);

        Set<ConstraintViolation<SimplePayloadDto>> violations = validator.validate(payload);

        assertFalse(violations.isEmpty());
    }
Code language: Java (java)

All good.

But wouldn’t it make sense to double check that the validator was definitely kicking out the expected error messages?

I probably wouldn’t do this at a unit test level on a real world project. The reason being is that I am highly confident that a validation library would be behaving and would be well tested, so me testing the code is pointless. I guess that’s context dependant, but I would anticipate catching failures like that further up my stack – at the level of integration or acceptance testing.

However, for learning purposes, it made sense to do it here.

Trying to figure out how to get the violation errors was a real challenge:

I put a breakpoint on the line after the violations object would have been set and dug in.

Nothing in there was really telling me what I wanted to see.

In my head I had it that I could compare direct objects… but that seemed fairly complex. I would have to new up instances of every violation I wanted to check for, and maybe consider the order that the validator kicked them out. Would they always be in the same order? Did that even matter outside of my test?

I will confess to falling back to ChatGPT for a solution here after a lot of head scratching.

The solution it gave me was one of them where I looked at it, and realised I had dismissed it as being a little too simplistic quite early on. Yet had spent about 20 minutes trying to find a ‘better’ (read: far more complicated) one, to no avail.

        assertTrue(violations
                .stream()
                .anyMatch(v -> v.getMessage().equals("Text must be provided")));Code language: Java (java)

Why I don’t like this is that if I had multiple Text properties, this might be far too vague.

But, hey, let’s worry about that then. Not right now.

So whilst I’m not thrilled with this solution, it was good enough for now, and I moved on.

Validating JSON Payloads In Controller Code

At this point we have a guarantee that the validation logic works.

The next step is to make things that little more complicated, by introducing a controller to receive the JSON payload, deserialise that JSON body into our DTO, and apply the validation logic.

All this happens outside of our direct control, by way of annotations.

Let’s see the code:

package uk.co.a6software.json_payload_validation.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.co.a6software.json_payload_validation.dto.SimplePayloadDto;

@RestController
@RequestMapping("/")
public class SimpleJsonPayloadController {

    @PostMapping("/no-payload")
    public ResponseEntity<String> handleNoPayload() {
        return ResponseEntity.ok("yes this worked");
    }
}

Code language: Java (java)

Again, much like when I was taking my first steps with the validation logic, I wanted a way to add in automated tests to confirm what was happening.

Earlier, we added in spring-boot-starter-test as a project dependency, so it is possible to write integration tests directly inside our project, and Spring Boot will take care of running them against our controller code.

Here’s an example:

// src/test/java/uk/co/a6software/json_payload_validation/controller/SimpleJsonPayloadControllerTest.java

package uk.co.a6software.json_payload_validation.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(SimpleJsonPayloadController.class)
public class SimpleJsonPayloadControllerTest {

    private final MockMvc mockMvc;

    @Autowired
    public SimpleJsonPayloadControllerTest(MockMvc mockMvc) {
        this.mockMvc = mockMvc;
    }

    @Test
    public void testHandleNoPayload() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/no-payload"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("yes this worked"));
    }
}Code language: Java (java)

I appreciate their is a whole lot of code here, and this would be even more confusing were I to show all of the test code up front.

Amazingly, all this code does is check that the /no-payload endpoint accepts a POST request and returns the string "yes this worked".

With such a simple concept to test, we can focus more on how the test works, rather than what it is testing.

Setting Up The Controller Test Class

First, we annotate the class with @WebMvcTest(SimpleJsonPayloadController.class):

@WebMvcTest(SimpleJsonPayloadController.class)
public class SimpleJsonPayloadControllerTest {Code language: Java (java)

This tells Spring Boot to create something akin to a unit test for our SimpleJsonPayloadController.

It’s specifically designed to focus on the web layer of the application, without loading the entire Spring context. This keeps the tests faster, because it doesn’t load up all the different beans configured elsewhere in the project.

Any beans / services / repos, etc that we do need, we need to manually mock, and annotate them with @MockBean. I haven’t had to do that yet, I just read that in the docs.

Likewise, we can do a more fully featured acceptance test, in which case we would want to use the @SpringBootTest annotation rather than @WebMvcTest. This is all good info to be aware of, but I do not yet have hands on experience doing that.

private final MockMvc mockMvc;

@Autowired
public SimpleJsonPayloadControllerTest(MockMvc mockMvc) {
    this.mockMvc = mockMvc;
}Code language: Java (java)

Then we inject an instance of MockMvc.

This class is what allows us to simulate HTTP requests and assert that their responses are what we expect.

As best I understand it, this doesn’t actually spin up a real server, making it faster than a more fully featured test.

@Test
public void testHandleNoPayload() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/no-payload"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("yes this worked"));
}Code language: Java (java)

Much like the code for working with the validation outcomes covered earlier, I found working with the MockMvc instance rather … challenging.

There’s a need to work with three different classes here:

  • MockMvc
  • MockMvcRequestBuilders
  • MockMvcResultMatchers

Why are two plural and one singular?

It’s a good question, and not one I have an answer for.

The code actually reads quite intuitively. Which is to say it is obvious what it does, when you read it.

However, writing it was an exercise in reading the docs, and making mistakes.

The gist is, we now have a test that can send in a POST request (without a body, but still), and then check that the outcome returns a 200 status code, and the expected string of "yes this worked".

A More Complex Example

Much like with DTO validation, we are going to now build upon the foundations we just laid.

I could have started to modify the /no-payload endpoint, but to get some practice into my fingers, I decided I’d just add in another endpoint.

This time the endpoint would expect a JSON payload making the test harder to write for a newbie.

The payload itself is really straightforward:

POST http://localhost:8080/simple-payload
Content-Type: application/json

{
  "text": "some text here"
}Code language: JavaScript (javascript)

And based on the presence or absence of text, we need to return a 200 or 400 response.

As before, we shall start with the easier of the two outcomes. The happy path.

This isn’t quite TDD, so let’s start with the controller handler for this request:

    @PostMapping("/simple-payload")
    public ResponseEntity<String> handleSimplePayload(
            @Valid @RequestBody SimplePayloadDto simplePayloadDto
    ) {
        return ResponseEntity.ok("yes this also worked");
    }
Code language: Java (java)

As above, I said I would make a new endpoint for handling a payload, so this time around we have a new endpoint called /simple-payload.

This receives the JSON body via a POST request. We covered all this already in a previous post, so I won’t be going into that code any further.

What we care about, for the purposes of this post, is the @Valid annotation.

The frustrating thing at this point is I can’t actually see how Spring Boot validation actually works. I tried putting breakpoints over my code, both in and out of a test driven environment, but I can’t figure out exactly what happens when the @Valid annotation is encountered.

That’s disappointing.

If you know how this works, please do let me know in the comments.

Here is the test code I came up with:

    @Test
    public void testHandleSimpleJsonPayload_withValidPayload() throws Exception {
        SimplePayloadDto validDto = new SimplePayloadDto("Some text");

        MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/simple-payload")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(validDto));

        mockMvc.perform(request)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("yes this also worked"));
    }
Code language: Java (java)

This is very similar to the previous test.

Where this differs is on lines 5-7. Here we craft a new post request, and set the content type and content body / payload.

Initially I had this:

        MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/simple-payload")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"text\":\"Some text\"}");Code language: Java (java)

In short, I was hardcoding my JSON string. But that was fairly awful thanks to the escaped quotes. I didn’t like the idea of that on larger objects.

Instead, I found I could use the ObjectMapper which is made available to us from the Jackson library, which is one of the dependencies brought in by spring-boot-starter-web. Behind the scenes I think this is what Spring Boot is using when objects are converted to JSON to be returned as response bodies. Not absolutely sure about that, but I’m sure I read it somewhere.

Anyway, we can leverage the library directly to make use of writeValueAsString to convert an object into a JSON string:

.content(objectMapper.writeValueAsString(validDto));

// is equivalent to

.content("{\"text\":\"Some text\"}");Code language: Java (java)

All combined, this gives us a passing happy path controller test.

Implementing & Testing The Unhappy Path

All the easy work is now done.

We only need to figure out how to write a test to cover the case where a payload is invalid, and, err, actually implement the way that response is produced.

The test here should be easy enough to write because, thanks to the earlier unit testing, we have a rough idea of what the validator will output. There may be differences in formatting, but this should be fixed easily enough based on the output from any failed test.

    @Test
    public void testHandleSimpleJsonPayload_withInvalidPayload() throws Exception {
        SimplePayloadDto invalidDto = new SimplePayloadDto(null);

        MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/simple-payload")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidDto));

        mockMvc.perform(request)
                .andExpect(MockMvcResultMatchers.status().is(400))
                .andExpect(MockMvcResultMatchers.content().string("Text must be provided"));
    }
Code language: Java (java)

We can then run this test and see what happens:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /simple-payload
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"13"]
             Body = {"text":null}
    Session Attrs = {}

Handler:
             Type = uk.co.a6software.json_payload_validation.controller.SimpleJsonPayloadController
           Method = uk.co.a6software.json_payload_validation.controller.SimpleJsonPayloadController#handleSimplePayload(SimplePayloadDto)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.bind.MethodArgumentNotValidException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = Invalid request content.
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Response content expected:<Text must be provided> but was:<>
Expected :Text must be provided
Actual   :
<Click to see difference>Code language: PHP (php)

Not the nicest output to copy / paste into the post, but the truth of it is it only looks slightly nicer in the console.

The gist of this is that we are getting the 400 error code, which is good.

But the expected error message is not there.

This is understandable, but I haven’t been able to work out why the MockHttpServletResponse.Body reports as empty in the console, and why the expected vs actual are not what I would expect.

If I send the request in to the controller, I do get back a response body:

{
  "timestamp": "2024-10-01T18:09:45.623+00:00",
  "status": 400,
  "error": "Bad Request",
  "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<?> uk.co.a6software.json_payload_validation.controller.SimpleJsonPayloadController.handleSimplePayload(uk.co.a6software.json_payload_validation.dto.SimplePayloadDto): [Field error in object 'simplePayloadDto' on field 'text': rejected value [null]; codes [NotNull.simplePayloadDto.text,NotNull.text,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [simplePayloadDto.text,text]; arguments []; default message [text]]; default message [Text must be provided]] \n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:144)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:224)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:178)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n",
  "message": "Validation failed for object='simplePayloadDto'. Error count: 1",
  "errors": [
    {
      "codes": [
        "NotNull.simplePayloadDto.text",
        "NotNull.text",
        "NotNull.java.lang.String",
        "NotNull"
      ],
      "arguments": [
        {
          "codes": [
            "simplePayloadDto.text",
            "text"
          ],
          "arguments": null,
          "defaultMessage": "text",
          "code": "text"
        }
      ],
      "defaultMessage": "Text must be provided",
      "objectName": "simplePayloadDto",
      "field": "text",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
    }
  ],
  "path": "/simple-payload"
}Code language: JSON / JSON with Comments (json)

Whilst that’s definitely not something you would want to send back to a real client, it is, nevertheless, not an empty string.

Confusing.

This implies Spring is, somehow, catching the validation error and converting it to a response. I’m wondering if that response that I see when booting the app and sending in a ‘real’ request is a ‘dev’ error, and I wouldn’t see something that detailed if the app were, somehow, running in Production mode?

Again, not sure.

These are things I haven’t yet learned. More documentation reading required, I feel.

Binding Result

When adding a @Valid annotation, we also get automatic access to a BindingResult object.

Now, I cannot find any good documentation about how this happens.

I actually only know this happens because IntelliJ autocomplete suggested this for me when I annotated my payload with @Valid.

That’s equal parts helpful and confusing… but I was able to find further examples about using BindingResult via Google.

Essentially, BindingResult stores the result of the validation process.

Here’s the initial stab at the code:

package uk.co.a6software.json_payload_validation.controller;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.co.a6software.json_payload_validation.dto.SimplePayloadDto;

@RestController
@RequestMapping("/")
public class SimpleJsonPayloadController {

    @PostMapping("/simple-payload")
    public ResponseEntity<?> handleSimplePayload(
            @Valid @RequestBody SimplePayloadDto simplePayloadDto,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body("It did not work");
        }

        return ResponseEntity.ok("yes this also worked");
    }
}
Code language: Java (java)

The BindingResult object gives us a useful method of checking whether or not validation succeeded – hasErrors().

From there, I return a 400 (bad request response), with the string body of “It did not work”.

This then fails the test as we would expect:

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"15"]
     Content type = text/plain;charset=UTF-8
             Body = It did not work
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Response content expected:<Text must be provided> but was:<It did not work>
Expected :Text must be provided
Actual   :It did not work
Code language: JavaScript (javascript)

Good stuff.

The thing we need to do now is extract the errors from the validation process – from bindingResult – and return them as some kind of JSON structure.

The updated test:

    public void testHandleSimpleJsonPayload_withInvalidPayload() throws Exception {
        SimplePayloadDto invalidDto = new SimplePayloadDto(null);

        MockHttpServletRequestBuilder request = MockMvcRequestBuilders.post("/simple-payload")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidDto));

        mockMvc.perform(request)
                .andExpect(MockMvcResultMatchers.status().is(400))
                .andExpect(MockMvcResultMatchers.content().string("{\"text\":\"Text must be provided\"}"));
    }
Code language: PHP (php)

A Map Of Errors

It would be nice if, at the start of any of my technical adventures, I had a Map of Errors. That would save a lot of wasted time.

Alas, I do not have such a thing.

And this is not that sort of map.

After a bit more Google searching I realised I could return JSON from a controller by creating a java.util.Map instance, where my keys and values would be String (for simplicity).

When I returned this, the Jackson library would automagically convert that object to a JSON string:

    @PostMapping("/simple-payload")
    public ResponseEntity<?> handleSimplePayload(
            @Valid @RequestBody SimplePayloadDto simplePayloadDto,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            Map<String, String> response = Map.of("some", "value");

            return ResponseEntity.badRequest().body(response);
        }

        return ResponseEntity.ok("yes this also worked");
    }
Code language: Java (java)

Which gives:

Expected :{"text":"Text must be provided"}
Actual   :{"some":"value"}Code language: JavaScript (javascript)

Therefore the ‘trick’ is to convert the errors contained inside the bindingResult into a nice Map<String, String> and we should be good.

Of course, I found this harder to do in practice than it sounded in theory.

The bindingResult object has a getFieldErrors() method. That returns a List<FieldError>.

So far, so good.

If we naively try to return this:

        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body(bindingResult.getFieldErrors());
        }Code language: Java (java)

Well, this fails because there is more data in the result than we want / expect:

Expected :{"text":"Text must be provided"}
Actual   :[{"codes":["NotNull.simplePayloadDto.text","NotNull.text","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["simplePayloadDto.text","text"],"arguments":null,"defaultMessage":"text","code":"text"}],"defaultMessage":"Text must be provided"," ...Code language: JavaScript (javascript)

However we are roughly on the right lines, as we do see the error message we want / expect buried in there. We can see that this is available to us under the defaultMessage key.

Therefore if we can iterate over that list, surely we could pull out that value by that key?

How do we iterate over a List?

This is Java, so there are plenty of ways to do that.

Here’s the first thing I tried:

package uk.co.a6software.json_payload_validation.controller;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.co.a6software.json_payload_validation.dto.SimplePayloadDto;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/")
public class SimpleJsonPayloadController {

    @PostMapping("/simple-payload")
    public ResponseEntity<?> handleSimplePayload(
            @Valid @RequestBody SimplePayloadDto simplePayloadDto,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                errors.put(fieldError.getField(), fieldError.getDefaultMessage());
            }
            return ResponseEntity.badRequest().body(errors);
        }

        return ResponseEntity.ok("yes this also worked");
    }
}
Code language: Java (java)

I had a bit of fun trying to work out which variety of Map was mutable. It turns out, HashMap is your friend here.

From there, I used a for-each loop to work my way over all the available errors, grabbing the data out of the current error and put‘ting that into my Map.

Finally I could return that object directly, as already covered.

This worked.

However, I knew it must be possible to get rid of the interstitial errors variable.

I wouldn’t say I am a code snob… well, I probably am, but I wouldn’t say I was.

When I see an imperative loop I tend to want to re-write it in a more declarative, functional style. Of course that makes my life harder here (in the short term), but I feel like it has longer term benefits.

Basically, rather than describing to the computer exactly how to work its way through the list (for-of), I instead tell the computer what I want it to do, and let it work out how best to do it. In order to do that, we need to work with stream.

I’m going to switch to screenshots rather than code snippets here, as it’s simply easier to visualise the problem I hit upon:

The code is quite unwieldy.

Definitely harder to read than the for-of version.

Convert the List to a Stream, then use the terminal operation of collect to put together a Map of data.

The process is identical really. Use the field as the key, and the default error message as the value.

Only, IntelliJ spots something:

IntelliJ function may return null but not allowed here

And it gives us a blue link to click to ‘Replace lambda with method reference’. That doesn’t fix the problem, but it makes the code more concise:

using method reference

The problem is that we can get a null here, whereas our Map says all values must be String.

Using the method reference denies me a way of solving this problem by using a ternary. If I convert back, I can use the lambda in combination with the tenary:

            Map<String, String> errors = bindingResult.getFieldErrors().stream()
                    .collect(Collectors.toMap(
                            FieldError::getField,
                            fieldError -> fieldError.getDefaultMessage() != null 
                                    ? fieldError.getDefaultMessage() 
                                    : "Default message"
                    ));
Code language: Java (java)

I was actually pretty happy with this.

It works.

It feels ugly as sin, but it works and the test finally passes.

I was wondering if there is a short hand, like in JavaScript, where I could do something like:

fieldError => fieldError.getDefaultMessage() ?? "Unknown error"Code language: JavaScript (javascript)

I wasn’t coming up with much, but when I asked ChatGPT to suggest possible refactorings it taught me something new.

Optionals.

import java.util.Optional;

// ...

        if (bindingResult.hasErrors()) {
            Map<String, String> errors = bindingResult.getFieldErrors().stream()
                    .collect(Collectors.toMap(
                            FieldError::getField,
                            fieldError -> Optional
                                    .ofNullable(fieldError.getDefaultMessage())
                                    .orElse("Unknown error")
                    ));Code language: Java (java)

I am starting to feel like a real Java programmer!

That, to me, is some of the most Java-ish code I have yet seen / (partially) written.

Now, I am aware that having this code directly inside the controller action is not ideal.

Yes, it works, and the tests pass.

But I would need to either copy / paste this into other controller actions as necessary, or extract this to a share method and call it all over the show.

I do know that is not the right approach. There exists another concept called Controller Advice which neatly solves this problem. So I will get on to that next.

Example Code

You can see the code over on GitHub.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.