CodeGym /Courses /Spring REST & MVC /Jackson 3 baseline JSON: Spring Boot 4

Jackson 3 baseline JSON: Spring Boot 4

Spring REST & MVC
Level 11, Lesson 0
Available

1. A fixed JSON baseline without bureaucracy

If you have ever assembled IKEA furniture without instructions, you already almost understand why a baseline is needed. On the surface, you have the parts (DTOs), the hands (controllers), and the screwdriver (Spring MVC), but without a clear “coordinate system” the result can become... an unexpected art object. It is the same in APIs: without a fixed JSON baseline, the contract starts to “drift,” and the client starts to suffer.

A fixed baseline is an agreement that saves nerves for you and for everyone who will use your API. In our course, that agreement is simple: we produce JSON through Jackson 3, and we work on Spring Boot 4. Not because “it is trendy,” but because the course must be internally consistent: code examples, serialization behavior, typical errors, date formats, enum behavior — everything should be predictable and the same.

A very practical beginner problem looks like this: you google “how Spring returns JSON,” find a tutorial from 2017, copy a chunk of configuration, and suddenly your project starts behaving strangely. Not because you are a bad developer, but because you mixed two different eras and two different library lines. As a result, you are not learning REST API, but dependency archaeology.

To “pin this down,” let us keep a simple table in mind (not for cramming, but as a reference):

Question What we treat as the course norm Why it matters
Which library is responsible for JSON Jackson 3 One predictable set of serialization/deserialization rules
Where this works Spring Boot 4 + Spring MVC The whole course is about the web/API layer on exactly this platform
How JSON is formed We return Java objects (DTOs) The contract is visible in code, not hidden in strings
What we do NOT do We do not assemble JSON manually as strings It is fragile, hard to read, and almost impossible to test properly

And the key idea of this lecture is: DTO is the shape of the contract, and Jackson is the mechanism that turns that shape into JSON. So for us, Jackson is not a “secondary library somewhere below”; it is part of API discipline.

2. Jackson in Spring MVC: its place in the chain

At this stage, it is easy to fall into magical thinking: you wrote @RestController, returned a DTO, and JSON “somehow appeared” in the response. Spring really does make this comfortable... but not because there is a magical JSON gnome living there; it is because there is a very specific mechanism for processing the response body. Right now, it is important to hold onto this as a basic anchor: without it, JSON starts to look like magic.

If we speak very practically, one chain matters to us: the controller method returns a Java object → then the web layer turns it into bytes in the HTTP response body. And if that object is supposed to become JSON, then Jackson must be part of that chain.

Here is a simplified diagram (without diving into framework internals):

flowchart TD
    C[HTTP client] -->|"GET /api/v1/tasks/{taskId}"| D[Spring MVC]
    D --> R["@RestController method"]
    R --> DTO[Response DTO]
    DTO --> J[Jackson 3]
    J --> B[JSON bytes in response body]
    B --> C

Notice that the controller in this diagram does not write JSON. It returns an object — and that is the key engineering point. Your controller is where you choose which DTO is the public contract. And Jackson is the library that, according to the serialization rules, turns that DTO into JSON text.

To feel this with your hands, let us make a tiny demonstration endpoint:

import com.example.tasktracker.api.dto.response.TaskSummaryResponse;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DemoController {

    @GetMapping("/api/v1/demo/task")
    TaskSummaryResponse demoTask() {
        // We return a DTO, not a string: Spring MVC will assemble JSON through Jackson
        return new TaskSummaryResponse("t-1", "Write docs", TaskStatus.TODO);
    }
}
// Jackson will produce JSON approximately like this: {"id":"t-1","title":"Write docs","status":"TODO"}

The point of the example is that you do not write JSON at all. You describe the response structure through the Java type TaskSummaryResponse, and from that Java type the JSON shape is derived automatically.

We need one anchor here: one baseline and one normal path for producing JSON. You design DTOs, and Jackson honestly turns them into JSON.

3. The Jackson 3 line in the project

When a project is small, it feels like: “well, what difference does it make which Jackson.” But as soon as you start growing an API, it suddenly turns out that mixing versions and approaches produces very strange effects: sometimes the IDE complains about incompatibility, sometimes you get an unexpected runtime error, sometimes serialization behaves “not like in the example.” So we need to state a simple discipline: Jackson must be exactly one line — Jackson 3.

In Spring Boot, most of the “JSON wiring” comes out of the box through the web starter. That is convenient, but convenience has a downside: if you manually add another Jackson version (or add an old one), you can create a “double foundation” for yourself. Like a house on two incompatible piles: it may stand, but it is scary.

In a training project, it is usually enough that Jackson arrives through the web stack. Here is a minimal fragment of build.gradle.kts (shortened so that the main point is visible):

dependencies {
    // Spring MVC + JSON (Jackson) comes from the starter, so we usually do not add Jackson separately
    implementation("org.springframework.boot:spring-boot-starter-webmvc")

    // Spring Boot test stack; versions and compatibility are pulled in by the BOM
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

What matters here is not that you memorize the lines, but the rule: we do not add JSON libraries “for reliability” if they are already there. An especially dangerous scenario is: “I will add jackson-databind manually because the tutorial says so.” In real development, you can do that, but then you are responsible for version compatibility. In a training project, this is almost always an extra point of failure.

If you want a very practical self-check method (without diving deeply into dependency management), you can look at which Jackson version was pulled in via Gradle. For example, like this (this is a command, not code):

# Shows which Jackson version is actually present in the project's dependency graph
./gradlew dependencyInsight --dependency jackson-databind

If you see that line 3.x was pulled in, then you are within the baseline. If you suddenly see 2.x, that almost certainly means someone “brought in an old dependency fragment from the internet” into the project. At that point, it is better to stop and fix it while the project is still small: later it will only hurt more.

Separately, let me state an important thing in the spirit of “Captain Obvious, but thanks”: we do not use Gson (or any “one more JSON library alongside it”) in this course. Not because Gson is bad, but because the course is built on one JSON behavior and one typical Spring MVC stack. The fewer alternatives you have, the faster you learn.

4. The controller: DTO instead of a JSON string

In programming, there is a temptation to “do it quickly and bluntly.” And one of the most insidious “quick-and-blunt” moves in a REST API is to return a string from a controller that merely “looks like JSON.” At first glance it works; at the second glance it starts breaking the contract, maintenance, and the faith in humanity in general.

The normal path in our project looks like this: the controller returns a DTO, and JSON is formed automatically at the web boundary. For example (simplified):

import com.example.tasktracker.api.dto.response.TaskSummaryResponse;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
class TaskDebugController {

    @GetMapping("/api/v1/tasks/{taskId}/summary")
    TaskSummaryResponse summary(@PathVariable String taskId) {
        // We take taskId from the URL, and return a DTO (the contract), not the internal model
        return new TaskSummaryResponse(taskId, "Write docs", TaskStatus.TODO);
    }
}

What is good here? The contract is visible right in the method signature: the endpoint returns TaskSummaryResponse. Jackson takes this DTO and writes it into JSON. The IDE, the compiler, and ordinary refactoring continue to work with the type, not with a string literal.

If you return a string with curly braces instead, the contract will be hidden inside the text. Such JSON does not refactor together with the code, it is easy to break syntactically, and Content-Type and the response shape start depending on manual decisions. So the basic rule is simple: the controller returns a DTO, not a JSON string.

And for the same reason, it is better not to expose the internal model “as is.” Jackson does not know what is public contract there and what is an internal implementation detail. It simply serializes what you gave it.

5. Predictable JSON from a Java type

Here it is useful to build a habit: when you look at a DTO, you should be able to roughly imagine the JSON shape of the response in advance. Not according to the principle of “I will run it and see,” but according to the principle of “I understand what I am promising the client.”

At the start, it is enough to keep several simple correspondences in mind: a DTO or record becomes a JSON object, List<T> becomes an array, enum becomes a string, numbers and boolean become their usual JSON values. That is already enough to avoid writing JSON by hand and to avoid guessing what exactly the client will see.

In the context of Task Tracker API, this means a simple thing: if an endpoint returns TaskSummaryResponse, you should be able to say which fields and in what general shape will go out. Jackson does not take this from thin air or from “Spring magic,” but from your Java model.

With this anchor, it becomes easier to discuss the tricky parts of the contract — how incoming JSON is read, how a date differs from an exact moment in time, why enum and null behave not “somehow,” but according to perfectly understandable rules.

6. Typical mistakes when fixing the Jackson baseline

In this topic, mistakes are almost never about “I wrote the annotation incorrectly,” but about “I made the wrong decision at the habit level.” So the mistakes look mundane and even “logical,” but later they become expensive. Let us walk through the most common ones so you can recognize them by smell before they start living in your project like cockroaches in a dormitory.

Mistake #1: mixing Jackson 2 and Jackson 3 because “the tutorial says so.”
This is a classic: you found an example, added a dependency by hand, and a different library line got into the project. Sometimes this shows up immediately (as a build error), sometimes as strange runtime behavior. The cure is simple and boring: keep one baseline, do not add JSON dependencies without a reason, and when in doubt, check what is actually on the classpath.

Mistake #2: returning a JSON string from a controller “for simplicity.”
At first it seems even “clearer”: here is the JSON, you can see it with your eyes. But in practice you lose the strongest part of Java — types and tooling support. A string does not refactor, it is not checked by the compiler, and it can be broken by a single comma. For a training and production-like REST API, this is a bad foundational habit.

Mistake #3: thinking that JSON means “less code,” so DTOs are optional.
When a person writes a JSON string, they really do have fewer classes. But they have more chaos: the contract is scattered across controllers, errors are copied, changes become risky, and the API stops being unified. In our project, DTOs are not decoration; they are the shape of the contract that you can hold onto and evolve.

Mistake #4: treating Jackson as “something out there somewhere” and not connecting it to the contract.
If you do not connect DTO and JSON, you will regularly be surprised: why did a field appear, disappear, or look “wrong.” In reality, Jackson does exactly what follows from the shape of your Java type. So the right mindset is: “I change the DTO — I change the JSON contract.”

Mistake #5: trying to “tune Jackson to my liking” right away without first fixing the default behavior.
This is especially tempting after a couple of conflicts with date formatting or field naming. But if you do not establish the basic model “controller returns an object → JSON is formed at the boundary,” early tuning turns into a chaotic fight with symptoms. Our task now is to understand and accept the baseline; after that, it will be easier to talk about the details of the JSON contract without unnecessary magic.

1
Task
Spring REST & MVC, level 11, lesson 0
Locked
DTO response instead of a JSON string
DTO response instead of a JSON string
1
Task
Spring REST & MVC, level 11, lesson 0
Locked
JSON array from a DTO list
JSON array from a DTO list
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION