Course topics

By WebNest Studio

Spring Boot Tutorial

JSON with Jackson 3

Almost every Spring Boot API speaks JSON, and the library doing the work is Jackson. Spring Boot 4 moves to Jackson 3, a major version with a new package name (tools.jackson), immutable mappers built with builders, unchecked exceptions and better defaults. Jackson 2 is still available in a deprecated form to ease migration.

This lesson shows how Spring Boot configures Jackson, how to control JSON output with annotations, how to customise the mapper globally with properties and customizers, how to write custom serializers, and what changes when you migrate from Jackson 2.

What Changed in Jackson 3

Most core classes moved from com.fasterxml.jackson to tools.jackson (for example tools.jackson.databind.json.JsonMapper), but the annotations you use daily — @JsonProperty, @JsonIgnore, @JsonInclude, @JsonFormat — stay in com.fasterxml.jackson.annotation, so most model classes compile unchanged. JsonMapper is immutable and built with JsonMapper.builder(). Jackson exceptions are now unchecked (JacksonException extends RuntimeException). Java time types are supported out of the box and dates are written as ISO-8601 strings by default.

Spring Boot Integration

Spring Boot auto-configures a JsonMapper bean used by Spring MVC, RestClient and WebClient. Customise it with spring.jackson.* properties or with a JsonMapperBuilderCustomizer bean (the Jackson 3 replacement for Jackson2ObjectMapperBuilderCustomizer). Custom serializers can be registered with @JacksonComponent (formerly @JsonComponent). Spring Boot keeps the helpful default of not failing on unknown JSON properties, so clients can send extra fields safely.

Controlling Output with Annotations

Annotations on your DTOs shape the JSON contract:

  • @JsonProperty("full_name") — rename a field in JSON.
  • @JsonIgnore — never serialise a field (e.g. an internal flag).
  • @JsonInclude(JsonInclude.Include.NON_NULL) — omit null fields.
  • @JsonFormat(pattern = "dd-MM-yyyy") — custom date format for one field.
  • @JsonPropertyOrder, @JsonAlias, @JsonUnwrapped, @JsonView — ordering, accepting old names, flattening, and per-endpoint field sets.
  • @JsonTypeInfo + @JsonSubTypes — polymorphic JSON (e.g. different payment method types).

DTOs Instead of Entities

Serialising JPA entities directly couples your API to your database schema, triggers lazy loading, and can leak fields such as password hashes. Map entities to records designed for the API (see the DTOs lesson). Records work perfectly with Jackson 3 without any annotations.

Examples

A DTO shaped with Jackson annotations

Java
import com.fasterxml.jackson.annotation.*;   // annotations keep their old package

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"id", "fullName", "email"})
public record StudentDto(
        Long id,
        @JsonProperty("fullName") String name,
        String email,
        @JsonFormat(pattern = "dd-MM-yyyy") LocalDate joined,
        Instant lastLogin,
        String phone,
        @JsonIgnore String internalNotes) {}

@GetMapping("/api/students/{id}")
public StudentDto get(@PathVariable Long id) {
    return new StudentDto(id, "Asha Rao", "asha@webnest.in", LocalDate.of(2026, 1, 15),
        Instant.parse("2026-09-27T08:30:00Z"), null, "VIP support");
}
Output
{"id":7,"fullName":"Asha Rao","email":"asha@webnest.in","joined":"15-01-2026","lastLogin":"2026-09-27T08:30:00Z"}
(phone omitted because it is null; internalNotes never serialised)

Global configuration with properties and a JsonMapperBuilderCustomizer

Java
# application.yml
spring:
  jackson:
    property-naming-strategy: SNAKE_CASE
    default-property-inclusion: non_null
    serialization:
      indent-output: true           # pretty-print (development only)

@Configuration
public class JacksonConfig {

    @Bean
    JsonMapperBuilderCustomizer jsonCustomizer() {
        return builder -> builder
            .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
            .defaultTimeZone(TimeZone.getTimeZone("Asia/Kolkata"));
    }
}
Output
{
  "id" : 7,
  "full_name" : "Asha Rao",
  "email" : "asha@webnest.in"
}

A custom serializer registered with @JacksonComponent

Java
// Serialise Money as {"amount":"1299.00","currency":"INR","display":"₹1,299.00"}
public record Money(BigDecimal amount, Currency currency) {}

@JacksonComponent
public class MoneyJson {

    public static class Serializer extends ValueSerializer<Money> {
        @Override
        public void serialize(Money value, JsonGenerator gen, SerializationContext ctxt) {
            NumberFormat fmt = NumberFormat.getCurrencyInstance(Locale.of("en", "IN"));
            fmt.setCurrency(value.currency());
            gen.writeStartObject();
            gen.writeStringProperty("amount", value.amount().setScale(2).toPlainString());
            gen.writeStringProperty("currency", value.currency().getCurrencyCode());
            gen.writeStringProperty("display", fmt.format(value.amount()));
            gen.writeEndObject();
        }
    }
}
Output
{"price":{"amount":"1299.00","currency":"INR","display":"₹1,299.00"}}

Polymorphic JSON and using JsonMapper directly

Java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = CardPayment.class, name = "card"),
    @JsonSubTypes.Type(value = UpiPayment.class, name = "upi")
})
public sealed interface Payment permits CardPayment, UpiPayment {}
public record CardPayment(String last4, String network) implements Payment {}
public record UpiPayment(String vpa) implements Payment {}

@Component
public class PaymentParser {

    private final JsonMapper mapper;   // tools.jackson.databind.json.JsonMapper (auto-configured)

    public PaymentParser(JsonMapper mapper) {
        this.mapper = mapper;
    }

    public Payment parse(String json) {
        return mapper.readValue(json, Payment.class);   // no checked IOException in Jackson 3
    }
}

paymentParser.parse("""
    {"type": "upi", "vpa": "asha@okbank"}
    """);
Output
UpiPayment[vpa=asha@okbank]

Common Mistakes

  • Importing com.fasterxml.jackson.databind.ObjectMapper in a Spring Boot 4 app and getting a different (Jackson 2) mapper than Spring MVC uses; inject tools.jackson JsonMapper instead.
  • Declaring a Jackson2ObjectMapperBuilderCustomizer in Boot 4 and wondering why it has no effect — use JsonMapperBuilderCustomizer.
  • Returning JPA entities directly and leaking internal fields or triggering lazy-loading exceptions.
  • Using the old spring.jackson.read.* / spring.jackson.write.* keys for JSON parser/generator features; in Spring Boot 4 they moved under spring.jackson.json.read.* and spring.jackson.json.write.*.
  • Leaving indent-output enabled in production, increasing response size.

Key Points to Remember

  • Spring Boot 4 uses Jackson 3: core classes live in tools.jackson, annotations stay in com.fasterxml.jackson.annotation.
  • JsonMapper is immutable and built with builders; Jackson exceptions are unchecked.
  • Configure with spring.jackson.* properties or a JsonMapperBuilderCustomizer bean.
  • Use @JsonProperty, @JsonInclude, @JsonFormat and @JsonIgnore to define the JSON contract on DTOs.
  • @JacksonComponent registers custom serializers; @JsonTypeInfo handles polymorphic JSON.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.