Spring BootでREST APIを書いていると、「LocalDateTimeが配列で返ってくる」「nullフィールドを消したい」「フロントがsnake_caseを要求してくる」みたいな場面に必ず遭遇しますよね。この記事では、Jacksonの設定をどのレイヤーで書けばいいのか、定番のアノテーションとObjectMapperのカスタマイズまでをまとめておきます。Spring Boot 3.x / Java 17 前提です。

Jackson設定の3つのレイヤー

Spring Bootは spring-boot-starter-web を入れると、Jacksonが自動構成され、ObjectMapper がBeanとして登録されます。設定する場所はおおまかに3つあります。

  • application.ymlspring.jackson.* プロパティ(アプリ全体)
  • アノテーション(DTOクラス単位・フィールド単位)
  • Jackson2ObjectMapperBuilderCustomizer などのBean定義(プログラム的に拡張)

全体に効かせたい挙動はYAMLで、特定のフィールドだけ変えたい場合はアノテーションで、と使い分けるのが基本方針です。

application.ymlでまとめて設定する

まずはYAMLでよく使う設定を見ておきます。これだけでも実務の8割は片付きます。

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Tokyo
    property-naming-strategy: SNAKE_CASE
    default-property-inclusion: non_null
    serialization:
      write-dates-as-timestamps: false
    deserialization:
      fail-on-unknown-properties: false

write-dates-as-timestamps: false を入れておくと、LocalDateTime[2026,5,24,...] のような数値配列ではなくISO-8601文字列で出力されるようになります。これに気付かず最初ハマる人が多い設定です。

日付・時刻のフォーマット

Java 8の時刻型は jackson-datatype-jsr310JavaTimeModule で扱われます。Spring Boot 3.xなら自動登録されているので、依存を追加しなくてもそのまま動きます。

フィールド単位でパターンを変えたい場合は @JsonFormat を使います。

public record OrderResponse(
    Long id,
    @JsonFormat(pattern = "yyyy-MM-dd")
    LocalDate orderDate,
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "Asia/Tokyo")
    ZonedDateTime createdAt
) {}

ZonedDateTime を使う場合は timezone 属性も指定しておくと、出力先のオフセットが安定します。

nullフィールドを除外する

レスポンスにnullを含めたくないケースはとても多いです。粒度別に使い分けましょう。

@JsonInclude(JsonInclude.Include.NON_NULL)
public record UserResponse(
    Long id,
    String name,
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    List<String> roles,
    String email
) {}
  • NON_NULL はnullだけ除外
  • NON_EMPTY は空文字・空コレクション・空Optionalも除外
  • NON_DEFAULT はプリミティブのデフォルト値(0false)も除外されるので扱いに注意

グローバルにかけたい場合は、先ほどの default-property-inclusion: non_null で十分です。

snake_case にしたいとき

フロントエンドが created_at のようなsnake_caseを期待することはよくあります。グローバルなら property-naming-strategy: SNAKE_CASE 一発、特定のフィールドだけ別名にしたければ @JsonProperty で上書きします。

public record ArticleResponse(
    Long id,
    String title,
    @JsonProperty("author")
    String authorName,
    LocalDateTime publishedAt
) {}

デシリアライズ側にも同じルールが適用されるので、リクエストもsnake_caseで受けられます。

Jackson2ObjectMapperBuilderCustomizer で拡張する

YAMLで表現できない設定や、独自モジュールの登録には Jackson2ObjectMapperBuilderCustomizer を使います。ObjectMapper を丸ごとBean定義で置き換える方法もありますが、Spring Bootのデフォルト挙動を巻き戻してしまうのでおすすめしません。

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> builder
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .modulesToInstall(new MoneyModule());
    }
}

この方式なら、Spring Bootが組み立てた ObjectMapper に差分だけを足せます。

カスタム Serializer / Deserializer

通貨表記や独自Enumなど、標準では表現できない変換は JsonSerializer<T> / JsonDeserializer<T> を実装します。例として BigDecimal¥1,000 形式で出力するシリアライザを書いてみます。

public class YenSerializer extends JsonSerializer<BigDecimal> {
    private static final NumberFormat FORMAT =
        NumberFormat.getCurrencyInstance(Locale.JAPAN);

    @Override
    public void serialize(BigDecimal value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        gen.writeString(FORMAT.format(value));
    }
}

フィールド単位で使うなら @JsonSerialize(using = YenSerializer.class) を付けます。アプリ全体で適用したい場合は SimpleModule に登録して、先ほどの modulesToInstall で渡します。

Enumを表示用ラベルから逆変換したいときは JsonDeserializer<T> を実装します。

public class StatusDeserializer extends JsonDeserializer<Status> {
    @Override
    public Status deserialize(JsonParser p, DeserializationContext ctx)
            throws IOException {
        String label = p.getText();
        return Status.fromLabel(label);
    }
}

出力したくないフィールドと読み取り専用

パスワードや内部IDのように、レスポンスに出したくないフィールドは @JsonIgnore で除外できます。さらに、入力としては受け取りたいが出力しないケースには @JsonProperty(access = ...) が便利です。

public record SignupRequest(
    String email,
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    String password
) {}

WRITE_ONLY ならレスポンス出力時に消え、READ_ONLY ならリクエスト受信時に無視されます。秘匿情報がうっかりレスポンスに混ざる事故を防げます。

未知プロパティをどう扱うか

リクエストJSONに知らないフィールドが含まれたとき、デフォルトでは UnrecognizedPropertyException が飛びます。クライアントの拡張に寛容にしたいAPIでは無効化しておくのが一般的です。

YAMLで deserialization.fail-on-unknown-properties: false を入れるか、クラス単位で @JsonIgnoreProperties(ignoreUnknown = true) を付けます。逆に内部APIや厳格に整合性を取りたいエンドポイントでは有効のままにしておくと、契約違反を早期に検知できます。

エラーレスポンスの形式を整えたい場合は、Spring BootのProblem Details (RFC 9457) でエラーレスポンスを統一する もあわせてどうぞ。また、CRUDの基本実装は Spring BootでREST APIを作るチュートリアル、DTO設計は MapStructでDTOマッピングを書く を参照してください。

まとめ

JSONの形を整える作業は、YAMLでざっくり全体方針を決めて、DTOにアノテーションでピンポイント調整を加えるのが王道です。それでも足りなくなったら Jackson2ObjectMapperBuilderCustomizer でモジュールを足したり、カスタム Serializer を書いたりすればよく、いきなり ObjectMapper をBeanで置き換える必要はほぼありません。設定が効かないときは「どのレイヤーで上書きされているか」を順に追っていくと、たいてい原因にたどり着けます。