DTOのJSON形式を確認するのに @WebMvcTest でControllerごとMockMvcにかけていませんか。Controllerのロジックが絡まない、ただの「シリアライズ結果が正しいか」を確認するだけなら、それはちょっと大掛かりすぎます。

そこで使うのが @JsonTest です。今回はJacksonTesterを使ってDTOのシリアライズ/デシリアライズ仕様をユニットテストレベルで固定する方法を、実例つきで整理します。

@JsonTestが起動するのはJacksonまわりだけ

@JsonTest を付けると、ObjectMapper やJacksonの Module、@JsonComponent として定義したBeanだけがロードされます。ControllerやServiceといったBeanはコンテキストに含まれません。

つまりMVCインフラを一切起動せず、「このDTOをJSONに変換したらどうなるか」だけを検証できるわけです。起動が軽い分、テストの実行も速くなります。

@WebMvcTest・@SpringBootTestとの起動範囲の違い

アノテーション対象起動範囲
@JsonTestJackson関連BeanObjectMapper・Module・@JsonComponentのみ
@WebMvcTestController層Web層のみ
@SpringBootTestアプリ全体全Bean

DTOの形式だけを見たい場合、わざわざ @SpringBootTest や @WebMvcTest を使う必要はありません。テストの目的に合わせて一番狭いスライスを選ぶのが基本です。この使い分けは テストアノテーション早見表 でも触れているので、他のスライスと迷ったらそちらも参考にしてください。

最小構成のテストを書く

spring-boot-starter-test にはAssertJのJSON拡張が含まれているので、追加の依存関係は不要です。テストクラスには @JsonTest を付けるだけです。

@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");
    }
}

JacksonTester<T> はフィールドに @Autowired で注入すれば初期化まで済ませてくれます。手動で JacksonTester.initFields(this, objectMapper) を呼ぶ必要はありません。

シリアライズ結果を検証する

固定のJSONファイルと比較する isEqualToJson() は分かりやすいですが、フィールドが増えるたびにファイルを更新するのは面倒です。フィールドの有無だけを見るなら hasJsonPathValue() が手軽です。

@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");
}

フィールド単位を検証する

特定フィールドの値だけを取り出して検証したいときは extractingJsonPathValue() 系のメソッドが便利です。JSONPathでネストしたオブジェクトや配列要素も指定できます。

@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 は型を問わず取得、extractingJsonPathStringValue や extractingJsonPathNumberValue は型を絞って取得できます。日付や配列など型が分かっているフィールドは、型を絞った方のメソッドを使うと意図しない型変換に気づきやすくなります。

日付フォーマットとnull除外設定の回帰を防ぐ

@JsonFormat で日付の出力形式を固定している場合、その形式が変わっていないかをテストで固定しておくと安心です。

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");
}

@JsonInclude(Include.NON_NULL) を設定漏れすると、nickname が null のままJSONに出力されてしまいます。doesNotHaveJsonPathValue() で「出ていないこと」を確認しておくと、この手の設定漏れをコードレビューより先に検知できます。

デシリアライズ結果を検証する

逆方向、JSON文字列からDTOへの変換も同じ JacksonTester で確認できます。parse() に渡すのはJSON文字列です。

@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");
}

フィールドが1つだけ気になる場合は getObject() の後にgetterやアクセサを呼ぶだけです。欠損フィールドがデフォルト値やnullでどう扱われるかも、この形でそのまま確認できます。

@JsonComponentのカスタムSerializer/Deserializerをテストする

@JsonComponent として定義したカスタムのSerializer/Deserializerは、@JsonTest のコンテキストにも自動で登録されます。テストコードを書き換える必要はありません。

@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");
}

普段のシリアライズテストと書き方は変わりません。カスタムSerializerが正しく登録されているかまで含めて検証できるのが @JsonTest の強みです。

application.ymlのJackson設定が反映される仕組み

@JsonTest は JacksonAutoConfiguration を利用するので、application.yml の spring.jackson.* 設定もテストにそのまま反映されます。

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

たとえばcamelCaseのフィールドを SNAKE_CASE に変換する設定を入れているなら、その挙動が壊れていないかも extractingJsonPathValue("$.registered_at") のようにキー名ごと検証しておけば、設定変更に気づかず本番に出してしまう事故を防げます。

ObjectMapperの取り違えに注意

@JsonTest でよくあるハマりどころが2つあります。1つは、テストの中で new ObjectMapper() を作って使ってしまうケースです。これでは application.yml の設定もカスタムModuleも反映されないので、必ず JacksonTester 経由か、@Autowired した ObjectMapper を使ってください。

もう1つは、@JsonComponent がコンポーネントスキャンの対象パッケージ外にある場合です。テストクラスがアプリのルートパッケージ配下にないと自動検出されないので、@Import でクラスを明示するか、パッケージ構成を見直しましょう。

まとめ

@JsonTest を使えば、DTOのシリアライズ/デシリアライズ仕様だけを軽量かつ高速にテストできます。日付フォーマットやnull除外、命名戦略、カスタムSerializer/Deserializerといった「地味だけど壊れやすい」仕様の回帰防止に向いています。

Controller全体の振る舞いまで確認したいときは @WebMvcTest や MockMvc に切り替えましょう。Jacksonの設定自体をもっと詳しく知りたい場合は Jacksonのカスタマイズガイド もあわせて読んでみてください。