Are you sure you don’t want to check with @WebMvcTest (@WebMvcTest), running the whole Controller through MockMvc just to check a DTO’s JSON format? If Controller logic isn’t involved and you just want to confirm “is the serialization result correct,” that’s a bit heavy-handed.

That’s where @JsonTest comes in. This time, I’ll organize with concrete examples how to use JacksonTester to pin down a DTO’s serialization/deserialization spec at the unit test level.

When you add @JsonTest, only the ObjectMapper, Jackson Modules, and Beans defined as @JsonComponent get loaded. Beans like Controllers and Services are not included in the context.

In other words, you can verify only “what happens when this DTO is converted to JSON” without booting any MVC infrastructure at all. Since the startup is lighter, test execution is faster too.

Startup scope differences from @WebMvcTest and @SpringBootTest

AnnotationTargetStartup scope
@JsonTestJackson-related BeansOnly ObjectMapper, Module, @JsonComponent
@WebMvcTestController layerWeb layer only
@SpringBootTestEntire applicationAll Beans

If you only want to look at the DTO’s format, there’s no need to bother with @SpringBootTest or @WebMvcTest. The basic rule is to choose the narrowest slice that fits your test’s purpose. This distinction is also covered in the test annotation cheat sheet, so if you’re torn between slices, check that out too.

Writing a minimal test

spring-boot-starter-test includes AssertJ’s JSON extensions, so no additional dependencies are needed. Just add @JsonTest to your test class.

@JsonTest
class UserDtoJsonTest {

    @Autowired
    JacksonTester<UserDto> json;

    @Test
    void serialize() throws Exception {
        UserDto dto = new UserDto(1L, "alice", "[email protected]");

        JsonContent<UserDto> result = json.write(dto);

        assertThat(result).isEqualToJson("user_dto.json");
    }
}

If you inject JacksonTester<T> into a field with @Autowired, initialization is handled for you. There’s no need to manually call JacksonTester.initFields(this, objectMapper).

Verifying serialization results

isEqualToJson(), which compares against a fixed JSON file, is easy to understand, but updating the file every time a field is added is tedious. If you just want to check whether a field is present, hasJsonPathValue() is a convenient option.

@Test
void serialize_hasEmail() throws Exception {
    UserDto dto = new UserDto(1L, "alice", "[email protected]");

    JsonContent<UserDto> result = json.write(dto);

    assertThat(result).hasJsonPathValue("$.email");
    assertThat(result).hasJsonPathStringValue("$.name");
}

Verifying individual fields

When you want to extract and verify just the value of a specific field, the extractingJsonPathValue() family of methods is handy. You can also use JSONPath to specify nested objects or array elements.

@Test
void serialize_extractName() throws Exception {
    UserDto dto = new UserDto(1L, "alice", "[email protected]");

    JsonContent<UserDto> result = json.write(dto);

    assertThat(result).extractingJsonPathStringValue("$.name")
        .isEqualTo("alice");
    assertThat(result).extractingJsonPathNumberValue("$.id")
        .isEqualTo(1);
}

extractingJsonPathValue retrieves the value regardless of type, while extractingJsonPathStringValue and extractingJsonPathNumberValue retrieve it with a specific type. For fields whose type is known, such as dates or arrays, using the type-specific method makes it easier to catch unintended type conversions.

Preventing regressions in date formatting and null-exclusion settings

If you’ve fixed the date output format with @JsonFormat, it’s reassuring to pin down that format in a test so you know it hasn’t changed.

public record UserDto(
    Long id,
    String name,
    @JsonFormat(pattern = "yyyy-MM-dd")
    LocalDate registeredAt,
    @JsonInclude(JsonInclude.Include.NON_NULL)
    String nickname
) {}
@Test
void serialize_dateFormatAndNullExclusion() throws Exception {
    UserDto dto = new UserDto(1L, "alice", LocalDate.of(2026, 1, 15), null);

    JsonContent<UserDto> result = json.write(dto);

    assertThat(result).extractingJsonPathStringValue("$.registeredAt")
        .isEqualTo("2026-01-15");
    assertThat(result).doesNotHaveJsonPathValue("$.nickname");
}

If you forget to set @JsonInclude(Include.NON_NULL), nickname will end up being output in the JSON as null. Confirming “it’s not present” with doesNotHaveJsonPathValue() lets you catch this kind of missed configuration before a code review does.

Verifying deserialization results

The reverse direction — converting from a JSON string to a DTO — can be verified with the same JacksonTester. What you pass to parse() is a JSON string.

@Test
void deserialize() throws Exception {
    String content = """
        {"id": 1, "name": "alice", "email": "[email protected]"}
        """;

    UserDto dto = json.parse(content).getObject();

    assertThat(dto.id()).isEqualTo(1L);
    assertThat(dto.name()).isEqualTo("alice");
}

If you only care about a single field, just call a getter or accessor after getObject(). You can also verify directly, in this same form, how missing fields are handled — whether they end up as default values or null.

Testing custom Serializers/Deserializers defined with @JsonComponent

Custom Serializers/Deserializers defined as @JsonComponent are automatically registered in the @JsonTest context as well. There’s no need to modify your test code.

@JsonComponent
public class MoneySerializer extends JsonSerializer<Money> {
    @Override
    public void serialize(Money value, JsonGenerator gen, SerializerProvider provider)
            throws IOException {
        gen.writeString(value.amount() + " " + value.currency());
    }
}
@Test
void serialize_customMoneySerializer() throws Exception {
    OrderDto dto = new OrderDto(1L, new Money(1000, "JPY"));

    JsonContent<OrderDto> result = orderJson.write(dto);

    assertThat(result).extractingJsonPathStringValue("$.money")
        .isEqualTo("1000 JPY");
}

The way you write the test is no different from a regular serialization test. Being able to verify that a custom Serializer is correctly registered, too, is one of @JsonTest’s strengths.

How Jackson settings in application.yml get applied

Since @JsonTest relies on JacksonAutoConfiguration, the spring.jackson.* settings in application.yml are applied to the test as-is.

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE
    default-property-inclusion: non_null

For example, if you’ve configured camelCase fields to be converted to SNAKE_CASE, verifying that behavior by key name as well — like extractingJsonPathValue("$.registered_at") — helps prevent shipping a configuration change to production without noticing it broke something.

Watch out for ObjectMapper mix-ups

There are two common pitfalls with @JsonTest. One is creating and using a new ObjectMapper() inside the test. This won’t reflect the settings from application.yml or your custom Modules, so always use one via JacksonTester or an @Autowired ObjectMapper.

The other is when @JsonComponent is outside the package targeted by component scanning. If the test class isn’t under the app’s root package, it won’t be auto-detected — either explicitly specify the class with @Import, or reconsider your package structure.

Summary

Using @JsonTest, you can test a DTO’s serialization/deserialization spec lightly and quickly, focused on just that concern. It’s well-suited for preventing regressions in specs that are “unglamorous but fragile” — things like date formatting, null exclusion, naming strategies, and custom Serializers/Deserializers.

When you want to verify the behavior of the entire Controller, switch to @WebMvcTest or MockMvc. If you want to learn more about Jackson configuration itself, check out the Jackson customization guide as well.