CodeGym /Courses /Spring REST & MVC /Jackson in @RestController

Jackson in @RestController

Spring REST & MVC
Level 8, Lesson 2
Available

1. Introduction

After choosing the converter, the picture has already become more honest: Spring looks at Content-Type, Accept, consumes / produces, and Java types. But that is still not enough to understand the JSON path completely. Even if Spring picked a JSON converter, someone still has to take the JSON text and turn it into a Java object — and then do the reverse on the response.

In our stack (Spring Boot 4 + Spring MVC), the JSON scenario is handled by Jackson. This is a separate library that knows how to turn JSON text into Java objects and back. Spring MVC manages the HTTP processing pipeline, chooses the converter, decides where to put the value, and what to return to the client. And Jackson is the “translator” between the world of JSON and the world of Java.

It is important to say this out loud, because almost all body-related problems boil down to one question: is it a Spring MVC problem, a converter selection problem, or JSON mapping on the Jackson side? If you do not separate that question, it becomes very easy to fix the wrong layer.

2. Jackson in the Spring MVC chain

In short, Spring MVC is the “director,” HttpMessageConverter is the “camera and microphone operator,” and Jackson is the “dialog translator.” Without the director, the actors may exist, but the scene will not happen. Without the operator, nothing will be heard or seen. Without the translator, the Russian text will not become English, even if the director tries very hard.

Let us pin down Jackson’s place in one simple diagram. It is useful to keep in mind when you debug POST /api/v1/tasks or any other JSON endpoint.

flowchart TD
    A[HTTP Request
body: bytes] --> B[Spring MVC] B --> C[HttpMessageConverter] C --> D["Jackson (JsonMapper)"] D --> E[Java object
CreateTaskBody] E --> F[Controller method] F --> G[Service layer] G --> H[Java object
TaskResponseBody] H --> I[HttpMessageConverter] I --> J["Jackson (JsonMapper)"] J --> K[HTTP Response
body: bytes]

From the responsibility point of view, this can be packed into a small table. The goal here is not to memorize the class names by heart; the goal is to stop mixing different levels into one “auto-conversion.”

Participant What it does in the JSON scenario What it does not do
Spring MVC Finds the controller method, assembles arguments, chooses the right converter by Content-Type/Accept and types Is not required to parse JSON “on its own”
HttpMessageConverter Reads/writes the body, calls a JSON library for a specific format Is not a “JSON parser”; it is only a wrapper and an integration point
Jackson (JsonMapper) Converts JSON ⇄ Java object (deserialization and serialization) Does not choose an endpoint, does not know about @GetMapping, does not manage statuses/headers

This leads to a natural question: why does all of this already work in a regular @RestController application without manually registering converters and building the JSON pipeline at a low level? Because when you have the standard MVC stack up and Jackson on the classpath, Spring Boot registers message converters that know how to work with JSON, and connects Jackson as the JSON engine for that path. That is why @RequestBody and returning objects in @RestController start working not “by telepathy,” but through the built-in application infrastructure.

Now we can briefly take Jackson out of the Spring picture and look at it separately: it is easier to see what exactly it does when reading and writing JSON.

Now an important version for real life: if your JSON is not being read, then the controller is not to blame. Most likely, the request never even reached it. The controller is already the next stage, where Spring gets only after it managed to assemble the arguments. Jackson is exactly where the request body is read before entering the method and the response body is written after leaving it.

3. Deserialization

The word “deserialization” sounds like a spell from an ancient manuscript, but in meaning it is quite straightforward. Deserialization is the process where you have text (JSON) and want to get an object (Java). In essence, it is the “unpacking” of data from a text format.

In Jackson 3, which we use on the Spring Boot 4 line in this course, the central object for JSON is called JsonMapper. Historically, many people know ObjectMapper in Jackson, but in our line we deliberately use JsonMapper to make it explicit: this is specifically about JSON.

Let us start with the most honest demonstration “in a vacuum,” without Spring. That makes it easier to see the essence: there is a string, there is a Java class, and there is a mapper that performs the transformation. Using the future Task Tracker API as an example, let us take a simple request body model for creating a task.

public class CreateTaskBody {
    // Request body DTO: what comes from JSON in the request body
    public String title;       // expecting the "title" key in JSON
    public String description; // expecting the "description" key in JSON
}

And now — reading JSON:

import tools.jackson.databind.json.JsonMapper;

public class JacksonReadDemo {
    public static void main(String[] args) throws Exception {
        // Source JSON that needs to be turned into a Java object
        String json = """
                {
                  "title": "Fix API",
                  "description": "Add docs"
                }
                """;

        // JsonMapper is Jackson's main object for JSON (de)serialization
        JsonMapper mapper = JsonMapper.builder().build();

        // readValue: read the JSON text and build an object of the required type
        CreateTaskBody body = mapper.readValue(json, CreateTaskBody.class);

        // Check that the fields were populated
        System.out.println(body.title);       // Fix API
        System.out.println(body.description); // Add docs
    }
}

There are three ideas in this example that a junior developer really needs right now.

First: Jackson reads text and creates an object of the required type. The type (CreateTaskBody.class) matters: it defines which fields are expected, what their types should be, and what the JSON should be converted into.

Second: Jackson does not need to know anything about Spring MVC. It does not know that this is POST /api/v1/tasks. It only sees a JSON string and the class it must convert it into.

Third: for mapping to work, the JSON must be “similar” to the Java class structure. Here “similar” is a very practical word: JSON keys (title, description) must match fields/setters, and the values must be type-compatible (a string in JSON should go into String, a number — into int/long, and so on). We will later discipline this part through DTOs and contract rules, but today it is important to see the principle itself: Jackson does not “guess,” it matches.

4. Serialization

Serialization is the reverse operation. There was an object — now there is a JSON string. And that is exactly what happens when your @RestController returns an object and Spring must write it to the response body in the application/json format.

Again, let us start with a small example without Spring. Suppose we have a response model:

public class TaskResponseBody {
    // Response DTO: what will be turned into JSON and sent to the response body
    public String id;    // "id" in JSON
    public String title; // "title" in JSON
}

Now let us create an object and convert it to JSON:

import tools.jackson.databind.json.JsonMapper;

public class JacksonWriteDemo {
    public static void main(String[] args) throws Exception {
        // Build the Java object we want to return to the client
        TaskResponseBody response = new TaskResponseBody();
        response.id = "t-1";
        response.title = "Fix API";

        // Jackson will look at the object's fields and generate JSON
        JsonMapper mapper = JsonMapper.builder().build();
        String json = mapper.writeValueAsString(response);

        // What will actually go over the network if this is returned from @RestController
        System.out.println(json); // {"id":"t-1","title":"Fix API"}
    }
}

Here you can get a very useful intuition: the JSON response in @RestController is not “what you wrote by hand.” It is what Jackson generated, looking at the fields (or getters) of the object you returned.

And from this follows an adult lesson that often saves time: when you change the Java response class, you change the JSON, even if you did not touch the controller method. That is why later in this course there will be a big module about DTOs and the JSON contract, but for now it is enough to understand the mechanism: Spring asked, “write the object as JSON,” and Jackson did it.

An important practical note: JsonMapper is usually configured once and reused. In the examples above we create it manually every time because the demonstration matters to us. In a real Spring Boot application, the Jackson mapper is created and configured by the platform, and then reused. Otherwise you will quickly end up with the option “I create a JsonMapper for every request,” which is roughly like “I buy a new kettle every time I need to boil water.” It works, but your wallet and CPU will be sad.

5. Two classes of JSON problems

When a JSON endpoint breaks, it is very important to first understand what exactly broke. There are two fundamentally different situations: the JSON text is structurally invalid (malformed), and the JSON is structurally valid, but it does not match what your Java type expects.

Malformed JSON is when the JSON syntax is violated. For example, an extra comma, an unclosed curly brace, or quotes in the wrong place. In that case Jackson cannot even start “mapping the fields”: first it has to be able to read the document as JSON.

Here is a demonstration:

import tools.jackson.databind.json.JsonMapper;

public class MalformedJsonDemo {
    public static void main(String[] args) {
        // Notice: the JSON is syntactically broken (extra comma)
        String badJson = """
                {
                  "title": "Fix API",
                }
                """;
        JsonMapper mapper = JsonMapper.builder().build();

        try {
            // At this point Jackson will fail even BEFORE mapping fields into an object
            mapper.readValue(badJson, CreateTaskBody.class);
        } catch (Exception e) {
            // In a real application, Spring MVC would return 400 Bad Request here
            System.out.println(e.getClass().getSimpleName()); // for example: JsonProcessingException
        }
    }
}

In Spring MVC this means one simple thing: the controller method may not run at all because the @RequestBody argument could not be assembled. This is not a business error and not an “error in the service” — it is a pure technical problem of reading the request body.

The second situation is when the JSON itself is valid, but it does not match the expected Java type. For example, you expect an object, but an array arrived. Or you expect a number, but the string "abc" arrived. Or you expect the title field, but the client sent taskTitle (and you have not agreed yet that this is acceptable). Formally, this is not “broken JSON,” but for your endpoint it is still an “unreadable” request: Jackson cannot correctly build an object of the required class.

Such errors also happen at the body conversion stage, that is, before the service logic. And that is exactly why we so insistently separate “read the body” from “process the business meaning.”

6. A mini example in Task Tracker API

Now we will connect everything into a small piece of code that looks like our real Task Tracker API. I want to emphasize an important methodological thought: today we are not interested in the perfect DTO design (that will come later), but in how Jackson works inside @RestController.

First — simple body models. They are deliberately “modest,” without complexity: we need to see the conversion.

public class CreateTaskBody {
    // Request DTO (comes from request body)
    public String title;
    public String description;
}

public class TaskResponseBody {
    // Response DTO (goes to response body)
    public String id;
    public String title;
}

Now — the controller. Pay attention to two places: @RequestBody (Jackson will read JSON) and TaskResponseBody in the response (Jackson will write JSON).

import java.net.URI;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class TaskController {

    // consumes/produces help Spring choose the right HttpMessageConverter (and therefore Jackson)
    @PostMapping(path = "/api/v1/tasks", consumes = "application/json", produces = "application/json")
    public ResponseEntity<TaskResponseBody> create(@RequestBody CreateTaskBody body) {
        // body is already ready: Jackson assembled it from the JSON request body
        TaskResponseBody response = new TaskResponseBody();
        response.id = "t-1";
        response.title = body.title;

        return ResponseEntity
                // Location header for the created resource
                .created(URI.create("/api/v1/tasks/" + response.id))
                // response will be serialized to JSON by Jackson
                .body(response);
    }
}

If you send such a request (for example, through a .http file), the picture will be as honest as possible:

### Create task
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
Accept: application/json

{
  "title": "Fix API",
  "description": "Add docs about converters"
}

What will happen step by step, without mysticism:

First, Spring MVC will see @PostMapping and understand that this is the appropriate handler for POST /api/v1/tasks. Then it will look at the method parameters and see @RequestBody CreateTaskBody body. After that, it will run HttpMessageConverter to read the request body. The JSON converter will invoke Jackson internally, and Jackson will build the CreateTaskBody object. And only after that will Spring call your create(...) method.

When the method returns ResponseEntity<TaskResponseBody>, Spring will again enable HttpMessageConverter, but this time for writing the response body. It will again use Jackson, but now in the opposite direction: it will turn TaskResponseBody into JSON text and put it in the response.

And that is, honestly, a rather calming mechanism. Because it becomes obvious: the controller does not need to read JSON manually. And it also does not need to manually form a JSON string. Its job is to work with Java types. The transformation “between worlds” is performed by the standard mechanism.

7. Typical mistakes when working with Jackson in Spring MVC

At the beginning, the most dangerous thing is not a compilation error, but a wrong “picture in your head.” It makes you fix the wrong place. Below are several mistakes that beginners make all the time, and that you will now be able to catch faster because you have a clear scheme: Spring MVC → converter → Jackson.

Mistake #1: confusing serialization and deserialization.
It seems minor, but then it produces phrases like “my serialization does not work” when reading a request. Keep a simple rule: when JSON enters the application and becomes an object, that is deserialization. When an object leaves the application and becomes JSON, that is serialization. Stop mixing up the words — and you will stop mixing up the direction of the problem.

Mistake #2: saying “Spring parses JSON” and forgetting about Jackson.
The phrase seems harmless, but it breaks diagnostics. When you understand that Jackson parses, you start checking the shape of the JSON and the expected Java type, instead of rewriting controller annotations “by guesswork.” Spring manages the process, but a specific library performs the JSON transformation.

Mistake #3: treating malformed JSON as a business error.
If the JSON is syntactically broken, neither the service nor the “task creation logic” is to blame. The request did not even pass the body-reading stage. This is a technical “could not read request body” error, and it happens before your code in the controller. In the future we will format such errors nicely with a unified contract, but the meaning will remain the same.

Mistake #4: trying to fix controller code when the request never reached it.
This is a classic trap: you put a breakpoint in create(...), but it does not fire, and you start thinking that “Spring somehow does not call the method.” In fact, Spring is not required to call the method if it could not assemble the arguments (for example, @RequestBody). That is why, in JSON input problems, the controller is often not “to blame” — the body conversion is.

Mistake #5: creating JsonMapper manually “just in case” everywhere.
After you see JsonMapper.builder().build(), the temptation appears: “Oh, I will parse JSON myself now.” That is almost always unnecessary. In a Spring Boot application, JSON conversion is already built into the pipeline. Manual mapper creation is appropriate for small demos (like in our lecture) or special tasks, but as the main style in controllers it quickly turns into a mess and duplicated framework work.

1
Task
Spring REST & MVC, level 8, lesson 2
Locked
Reading JSON into an object using `JsonMapper`
Reading JSON into an object using `JsonMapper`
1
Task
Spring REST & MVC, level 8, lesson 2
Locked
Writing an object to JSON with `JsonMapper`
Writing an object to JSON with `JsonMapper`
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION