HttpMessageNotReadableException は、@RequestBody で受け取るリクエストの本文(JSON)を Java のオブジェクトに変換できなかったときの例外です。 Spring は 400 Bad Request を返し、ログに次のような WARN を1行出します。

WARN ... o.s.w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `int` from String "abc": not a valid `int` value]

直し方は、JSON parse error: の後ろの文(Jackson の例外の文)を読んで、送る側の JSON か、受け取る側のクラスのどちらかを合わせることです。 文の書き出しで原因がほぼ決まります。

メッセージの書き出し 原因 対処
Required request body is missing 本文が空(または null) 本文を送る。任意なら @RequestBody(required = false)
Unexpected character ('}' … JSON の書き方の誤り(末尾のカンマ・引用符) 送る側の JSON を直す
Unrecognized token 'name' JSON ではない本文(フォーム形式など) JSON.stringify で送る
Unexpected end-of-input JSON が途中で切れている 閉じ括弧・送信サイズを確かめる
Cannot deserialize value of type `int` from String "abc" 数値の項目に数値でない文字列 型を合わせる
Cannot map `null` into type `int` プリミティブ型に null、record では項目の省略でも出る Integer などのラッパー型にする
… `java.time.LocalDate` from String "2026/10/13" 日付の形式が違う ISO 形式で送るか @JsonFormat
not one of the values accepted for Enum class enum に無い値・大文字小文字の違い 値を合わせるか設定で許す
Cannot deserialize value of type `java.util.ArrayList<…>` from Object value 配列とオブジェクトの取り違え […] と {…} を合わせる
Trailing token … found after value JSON が2つ続いている 1つにする
Invalid UTF-8 start byte 0x8e Shift_JIS などで送った UTF-8 で送るか charset を付ける
Cannot construct instance of … (although at least one Creator exists) 引数なしコンストラクタが無いクラス record にする・-parameters・Kotlin モジュール

Spring Boot 4 の Jackson は 3 系(パッケージが tools.jackson)です。 Boot 3 までの Jackson 2 と既定値が違い、Boot 3 では通っていた JSON が Boot 4 で 400 になることがあります(下の「Boot 4 で変わった点」)。

確認環境:Spring Boot 4.1.1(Spring Framework 7.0.9、Jackson 3.1.5 は Boot の依存管理の版)、OpenJDK 25.0.4.1(Linux)。Jackson 3.1.5(jackson-core・jackson-databind)は GitHub のタグのソースからビルドし、この記事の表の JSON を実際に読ませて例外のクラスとメッセージを取りました。Spring 側のメッセージ(JSON parse error:・Required request body is missing:・ログの Resolved [ の形)と 400 になる流れは、Spring Framework v7.0.9 のソース(AbstractJacksonHttpMessageConverter・RequestResponseBodyMethodProcessor・DefaultHandlerExceptionResolver)で確かめました。Boot の既定値は v4.1.1 のソースとドキュメントで確かめています(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「HttpMessageNotReadableExceptionの直し方|原因一覧」

メッセージの読み方:Spring の部分と Jackson の部分

ログの1行は、3つの部分でできています。

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `int` from String "abc": not a valid `int` value]
         └───────────── Spring の例外 ──────────────────────┘ └ Spring ┘ └──────────── Jackson の例外の文 ────────────┘

Spring Framework 7.0.9 の AbstractJacksonHttpMessageConverter は、Jackson の例外を受けて次のように包み直しています。

catch (InvalidDefinitionException ex) {
    throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JacksonException ex) {
    throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}

ここから分かることは3つです。

分かること 理由
JSON parse error: の後ろは Jackson の文そのまま ex.getOriginalMessage() をつなげている
ログには「どの項目か」が出ないことがある getOriginalMessage() は、Jackson の文の末尾の (through reference chain: UserForm["age"]) を含まない
Type definition error: は 400 ではない HttpMessageConversionException は DefaultHandlerExceptionResolver の対象外で、500 になる

項目の場所を知りたいときは、原因の例外(ex.getCause())を見ます。 Jackson 3.1.5 で取った、同じ失敗の全文です。

Cannot deserialize value of type `int` from String "abc": not a valid `int` value
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #7] (through reference chain: com.example.demo.UserForm["age"])

byte offset: #7 は本文の先頭から何バイト目か、reference chain はどの項目かです。Source: REDACTED は、本文の中身をメッセージに入れない Jackson の既定の動きです(個人情報がログに出るのを防ぐため)。

原因1:本文が空(Required request body is missing)

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.example.demo.UserResponse com.example.demo.UserController.create(com.example.demo.UserForm)]

この文は Jackson ではなく Spring が出します。 RequestResponseBodyMethodProcessor は、変換の結果が null で、@RequestBody が必須(required = true、既定)のときに、メソッドのシグネチャを付けて投げます。

よくある状況 確かめること
fetch・axios で body を付け忘れた ブラウザの開発者ツールの「ペイロード」
GET で @RequestBody を使っている GET の本文はクライアント・プロキシで落ちることがある。検索条件は @RequestParam にする
本文が文字列 null Jackson は null を null に変換し、Spring が「無い」と判断する
フィルターで本文を先に読んでしまった 本文のストリームは1回しか読めない。ログ用のフィルターは ContentCachingRequestWrapper を使う

本文が任意なら、@RequestBody(required = false) にして、引数が null のときの処理を書きます。

原因2:JSON の書き方の誤り(StreamReadException)

Jackson 2 の JsonParseException は、Jackson 3 では tools.jackson.core.exc.StreamReadException になりました(Jackson 3.1.5 のソースに JsonParseException はありません)。実際に読ませた結果です。

送った本文 Jackson 3.1.5 の文
{"name":"a",} Unexpected character ('}' (code 125)): was expecting double-quote to start property name
{name:"a"} Unexpected character ('n' (code 110)): was expecting double-quote to start property name
{'name':'a'} Unexpected character (''' (code 39)): was expecting double-quote to start property name
name=a&age=1 Unrecognized token 'name': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
{"name":"a"}x Unrecognized token 'x': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
{"name":"a" Unexpected end-of-input: expected close marker for Object (…)

Unrecognized token 'name' は、本文が JSON でない典型です。 jQuery の $.ajax で data にオブジェクトを渡すと、既定ではフォーム形式(name=a&age=1)で送られます。JSON.stringify(data) で文字列にして送ります。

await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "山田", age: 30 }),
});

Unexpected end-of-input は、JSON を手で組み立てて括弧が足りないとき、またはプロキシなどで本文が途中で切れたときに出ます。

Content-Type を付け忘れたときは、この例外ではありません。 application/json 以外(text/plain など)だと HttpMediaTypeNotSupportedException で 415 になります。

原因3:型が合わない(InvalidFormatException・MismatchedInputException)

受け取るクラスの例です。

public record UserForm(String name, Integer age, LocalDate birthday, Status status, Boolean admin, List<String> tags) {}
public enum Status { ACTIVE, INACTIVE }
送った値 例外 Jackson 3.1.5 の文
"age":"abc" InvalidFormatException Cannot deserialize value of type `java.lang.Integer` from String "abc": not a valid `java.lang.Integer` value
"age":99999999999(int の項目) InputCoercionException Numeric value (99999999999) out of range of `int` (-2147483648 - 2147483647)
"admin":"yes" InvalidFormatException Cannot deserialize value of type `boolean` from String "yes": only "true"/"True"/"TRUE" or "false"/"False"/"FALSE" recognized
"tags":"a" MismatchedInputException Cannot deserialize value of type `java.util.ArrayList<java.lang.String>` from String value (token `JsonToken.VALUE_STRING`)
"name":{"first":"a"} MismatchedInputException Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)

InvalidFormatException は「値の形が違う」、MismatchedInputException は「JSON の種類(文字列・配列・オブジェクト)が違う」と覚えると区別しやすくなります。 InvalidFormatException は MismatchedInputException の子クラスです。

逆に、次の値はエラーにならずに変換されました。 気づかないまま値が変わることがあります。

送った値 変換の結果
"age":"30"(数字の文字列) 30
"age":1.5 1(小数点以下が切り捨て)
"name":"a","email":"x"(クラスに無い項目) email は無視される

小数を整数に入れたくないときは spring.jackson.deserialization.accept-float-as-int=false で、1.5 をエラーにできます(Jackson 3.1.5 の既定は true)。

原因4:プリミティブ型と null(Boot 4 で増えたエラー)

JSON parse error: Cannot map `null` into type `int` (set `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to 'false' to allow)

Jackson 3 では FAIL_ON_NULL_FOR_PRIMITIVES の既定が true になりました。 int・boolean などのプリミティブ型に null が来るとエラーです。Jackson 3.1.5 で試した結果は次のとおりです。

受け取るクラス 送った本文 結果
record Prim(String name, int age, boolean admin) {"name":"a"}(age を省略) エラー(Cannot map `null` into type `int`)
同じ record {"name":"a","age":1,"admin":true} 成功
フィールドが public int age のクラス {"name":"a"}(age を省略) 成功(age は 0)
同じクラス {"name":"a","age":null} エラー

record(やコンストラクタで受け取るクラス)では、項目を省略しただけでもエラーになります。 コンストラクタの引数に渡す値が無いと、Jackson は null を渡そうとするためです。Boot 3 では通っていた画面が、Boot 4 にしたら 400 になる、という形でよく出ます。

直し方は3つあります。

方法 書き方 向いている場面
ラッパー型にする(おすすめ) Integer age・Boolean admin 省略を「未入力」として扱いたい
必須にして入力チェック @NotNull Integer age と @Valid 省略を 400 にしたいが、文を分かりやすくしたい
機能を切る spring.jackson.deserialization.fail-on-null-for-primitives=false Boot 3 と同じ動きに戻したい

Boot 4 には、Jackson 2 に近い既定値へまとめて戻す spring.jackson.use-jackson2-defaults=true もあります。Jackson 3.1.5 の configureForJackson2() で、上の record に {"name":"a"} を読ませると、age が 0 で成功しました。入力チェックの書き方は関連記事「Spring Bootバリデーション一覧|@Validと@Validated」で扱っています。

原因5:日付・時刻の形式(InvalidFormatException)

LocalDate・LocalDateTime は、既定では ISO 8601 の形(2026-10-13・2026-10-13T09:00:00)で受け取ります。Jackson 3 では日付・時刻の型の対応が jackson-databind に入っており、Jackson 2 のように jackson-datatype-jsr310 を足す必要はなくなりました。

型 送った値 結果
LocalDate "2026-10-13" 成功
LocalDate "2026/10/13" エラー(… could not be parsed at index 4)
LocalDate "2026-02-30" エラー(Invalid date 'FEBRUARY 30')
LocalDate "1995-04-01T00:00:00" 成功(時刻は捨てられる)
LocalDate 19950401(数値) 成功するが +56592-05-05(1970年1月1日からの日数として読まれる)
LocalDateTime "2026-10-13 09:00:00"(空白区切り) エラー(… could not be parsed at index 10)
LocalDateTime "2026-10-13"(時刻なし) エラー
LocalDateTime "2026-10-13T09:00:00Z" 成功(Z は捨てられる)

"2026/10/13" のときの全文は次のとおりです。

JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String "2026/10/13": Failed to deserialize `java.time.LocalDate` (with format 'Value(Year,4,10,EXCEEDS_PAD)'-'Value(MonthOfYear,2)'-'Value(DayOfMonth,2)'): (java.time.format.DateTimeParseException) Text '2026/10/13' could not be parsed at index 4

index 4 は「先頭から5文字目で読めなくなった」という意味です。 2026/ の / で止まっています。

送る側を ISO 形式にできないときは、項目に @JsonFormat を付けます。

import com.fasterxml.jackson.annotation.JsonFormat;

public record SearchForm(
        @JsonFormat(pattern = "yyyy/MM/dd") LocalDate from,
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt) {}

@JsonFormat は Jackson 3 でも com.fasterxml.jackson.annotation のままです。 Jackson 3 は注釈のライブラリ(jackson-annotations)を 2 系のまま使うためです。@JsonFormat(pattern = "yyyy/MM/dd") を付けると、今度は "2026-10-13" がエラーになりました(両方を受けたいときは独自のデシリアライザが要ります)。

spring.jackson.date-format は java.util.Date 用の設定で、LocalDate には効きません。 LocalDate の形式は @JsonFormat で決めます。日付の扱い全般は関連記事「Jackson ObjectMapperの使い方|JSON変換・日付・record」で扱っています。

原因6:enum に無い値

JSON parse error: Cannot deserialize value of type `com.example.demo.Status` from String "active": not one of the values accepted for Enum class: [INACTIVE, ACTIVE]
送った値 結果(Jackson 3.1.5 の既定)
"ACTIVE" 成功
"active"(小文字) エラー(大文字・小文字を区別する)
"DELETED"(定義に無い) エラー
""(空文字) エラー(Cannot coerce empty String ("") to `…Status` value)
5(数値) エラー(index value outside legal index range [0..1])

[INACTIVE, ACTIVE] の並びは、enum の定義順とは限りません。 受け付ける値の一覧として読みます。

したいこと 設定(application.yml の形) Jackson 3.1.5 で試した結果
小文字も受け付ける spring.jackson.mapper.accept-case-insensitive-enums: true "active" → ACTIVE
知らない値は null にする spring.jackson.datatype.enum.read-unknown-enum-values-as-null: true "DELETED" → null
知らない値は決めた定数にする 定数に @JsonEnumDefaultValue を付け、spring.jackson.datatype.enum.read-unknown-enum-values-using-default-value: true "Z" → UNKNOWN(@JsonEnumDefaultValue を付けた定数)

1つの enum だけ変換のしかたを変えたいときは、enum に @JsonCreator を付けた static メソッドを書き、文字列から定数を返します。

原因7:配列とオブジェクトの取り違え・JSON が2つ

受け取る型 送った本文 Jackson 3.1.5 の文
UserForm [{"name":"a"}] Cannot deserialize value of type `…UserForm` from Array value (token `JsonToken.START_ARRAY`)
List<UserForm> {"name":"a"} Cannot deserialize value of type `java.util.ArrayList<…UserForm>` from Object value (token `JsonToken.START_OBJECT`)
Map<String, Object> [1] Cannot deserialize value of type `java.util.LinkedHashMap<java.lang.String,java.lang.Object>` from Array value (token `JsonToken.START_ARRAY`)
UserForm {"name":"a"}{"name":"b"} Trailing token (`JsonToken.START_OBJECT`) found after value (bound as `…UserForm`): not allowed as per `DeserializationFeature.FAIL_ON_TRAILING_TOKENS`

from Array value は「配列が来たがオブジェクトを期待していた」、from Object value はその逆です。 1件を送るか一覧を送るかを、画面と API で揃えます。

後ろに余計な JSON があるときのエラーも、Jackson 3 から既定で出ます(FAIL_ON_TRAILING_TOKENS の既定が true)。

原因8:文字コード(Invalid UTF-8 start byte)

JSON parse error: Invalid UTF-8 start byte 0x8e

{"name":"山田"} を Shift_JIS のバイト列で送ると、このエラーになりました。JSON の文字コードは UTF-8 が標準です(RFC 8259)。

Spring Framework 7.0.9 のソースでは、Content-Type の charset が UTF-8・UTF-16・UTF-32 以外のとき、その charset の Reader で読みます。既存のシステムから Shift_JIS で送られてくる場合、送る側が Content-Type: application/json;charset=Shift_JIS を付ければ読めます。付けられないときは送る側を UTF-8 にします。

原因9:クラスを作れない

受け取るクラス 例外 Spring の扱い
引数ありコンストラクタだけ(-parameters なしでコンパイル) MismatchedInputException:Cannot construct instance of `…` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) 400
同じクラスを -parameters 付きでコンパイル 成功 -
抽象クラス・インターフェース InvalidDefinitionException:Cannot construct instance of `…` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, … 500(Type definition error: [simple type, class …])
static でない内部クラス InvalidDefinitionException:… non-static inner classes like this can only by instantiated using default, no-argument constructor 500
record 成功(-parameters が無くても、record の項目名を使う) -

引数ありコンストラクタのクラスは、引数の名前がクラスファイルに残っていれば読めます。 Spring Boot の Gradle プラグインと Maven の親 POM は、既定で -parameters を付けてコンパイルします。IDE の設定で外れている、自前の pom で親を使っていない、という場合にこのエラーになります。

Kotlin の data class も同じ文で失敗します。 jackson-module-kotlin(Jackson 3 の座標は tools.jackson.module:jackson-module-kotlin)を依存に足すと、Boot が自動で登録します(spring.jackson.find-and-add-modules の既定が true)。start.spring.io で Kotlin を選ぶと最初から入っています。

record の compact コンストラクタで投げた例外も、この例外になります。

public record Range(LocalDate from, LocalDate to) {
    public Range {
        if (from != null && to != null && from.isAfter(to)) {
            throw new IllegalArgumentException("from は to と同じか前の日付にします");
        }
    }
}
JSON parse error: Cannot construct instance of `com.example.demo.Range`, problem: from は to と同じか前の日付にします

例外は ValueInstantiationException(JacksonException の仲間)なので 400 です。Cannot construct instance の原因の全体は、関連記事「Jackson Cannot construct instanceの直し方」で扱っています。

原因10:知らない項目(Boot 4 の既定では起きない)

JSON parse error: Unrecognized property "email" (class com.example.demo.Plain), not marked as ignorable

Jackson 3 では FAIL_ON_UNKNOWN_PROPERTIES の既定が false です。 クラスに無い項目が来ても無視されます。上の文は、わざと true にしたときの結果です(Jackson の全文には (2 known properties: "age", "name") が続きますが、Spring のログには出ません)。項目名の誤りに気づきたいときは spring.jackson.deserialization.fail-on-unknown-properties=true にします。詳しくは関連記事「Jackson UnrecognizedPropertyExceptionの直し方」で扱っています。

Boot 4 で変わった点(Jackson 2 → 3)

項目 Boot 3(Jackson 2) Boot 4(Jackson 3)
パッケージ com.fasterxml.jackson.databind tools.jackson.databind
注釈のパッケージ com.fasterxml.jackson.annotation 同じ(2 系のまま)
例外の親 JsonProcessingException(検査例外) JacksonException(RuntimeException)
JSON の書き方の誤り JsonParseException StreamReadException
プリミティブに null 0 になる(Boot 3 の既定) エラー(FAIL_ON_NULL_FOR_PRIMITIVES が true)
JSON の後ろに余計な値 無視(Jackson 2 の既定) エラー(FAIL_ON_TRAILING_TOKENS が true)
知らない項目 無視(Boot が切っていた) 無視(Jackson 3 の既定)
日付・時刻の型 jackson-datatype-jsr310 が要る databind に入っている

Boot 4.1.1 のドキュメントには「Jackson 3 is the preferred and default library.」とあり、Jackson 2 の対応は非推奨です。@ControllerAdvice などで Jackson の例外クラスを import しているコードは、Boot 4 に上げるとパッケージ名の変更でコンパイルできなくなります。移行の全体は関連記事「Spring Boot 4移行の手順|3から4の変更点一覧とチェックリスト」で扱っています。

400 と分かりやすいメッセージを返す(@RestControllerAdvice)

既定のままだと、400 の本文は次の形で、理由が分かりません(server.error.include-message の既定が never のため)。

{"timestamp":"2026-10-13T00:00:00.000Z","status":400,"error":"Bad Request","path":"/api/users"}

ResponseEntityExceptionHandler を継承して handleHttpMessageNotReadable を上書きすると、ProblemDetail(RFC 9457 のエラー形式)で項目名と理由を返せます。

package com.example.demo;

import java.util.Arrays;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import tools.jackson.core.JacksonException;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.exc.ValueInstantiationException;

@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(ApiExceptionHandler.class);

    @Override
    protected @Nullable ResponseEntity<Object> handleHttpMessageNotReadable(
            HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

        Throwable cause = ex.getCause();
        // ログには Jackson の全文(項目の場所・バイト位置つき)を残す
        log.warn("Request body could not be read: {}", cause != null ? cause.getMessage() : ex.getMessage());

        ProblemDetail body = ProblemDetail.forStatusAndDetail(status, toUserMessage(cause));
        body.setTitle("リクエストの本文を読み取れません");
        if (cause instanceof JacksonException jex && !jex.getPath().isEmpty()) {
            body.setProperty("field", path(jex));
        }
        return handleExceptionInternal(ex, body, headers, status, request);
    }

    // 抽象クラスを受け取ろうとした等の「作りの誤り」は 500 のまま、ログに残す
    @ExceptionHandler(HttpMessageConversionException.class)
    ProblemDetail handleConversion(HttpMessageConversionException ex) {
        log.error("Message conversion failed", ex);
        return ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    static String toUserMessage(@Nullable Throwable cause) {
        if (cause instanceof InvalidFormatException ex) {
            String field = path(ex);
            Class<?> target = ex.getTargetType();
            if (target != null && target.isEnum()) {
                String allowed = Arrays.stream(target.getEnumConstants())
                        .map(Object::toString).collect(Collectors.joining(", "));
                return field + " は " + allowed + " のいずれかで指定します(送られた値: " + ex.getValue() + ")";
            }
            return field + " の形式が正しくありません(送られた値: " + ex.getValue() + ")";
        }
        if (cause instanceof ValueInstantiationException ex && ex.getCause() != null) {
            return ex.getCause().getMessage();
        }
        if (cause instanceof MismatchedInputException ex && !ex.getPath().isEmpty()) {
            return path(ex) + " の型が正しくありません";
        }
        if (cause instanceof JacksonException) {
            return "JSON の書き方が正しくありません";
        }
        return "リクエストの本文がありません";
    }

    // [items, 1, qty] → items[1].qty
    static String path(JacksonException ex) {
        StringBuilder sb = new StringBuilder();
        for (JacksonException.Reference ref : ex.getPath()) {
            if (ref.getPropertyName() != null) {
                if (!sb.isEmpty()) sb.append('.');
                sb.append(ref.getPropertyName());
            }
            else if (ref.getIndex() >= 0) {
                sb.append('[').append(ref.getIndex()).append(']');
            }
        }
        return sb.toString();
    }
}

toUserMessage と path の部分は、Jackson 3.1.5 で実際に例外を起こして結果を確かめました。

送った本文 返る detail
{"status":"DONE"} status は ACTIVE, INACTIVE のいずれかで指定します(送られた値: DONE)
{"items":[{"qty":1},{"qty":"two"}]} items[1].qty の形式が正しくありません(送られた値: two)
{"items":[{"qty":{}}]} items[0].qty の型が正しくありません
{"customer":"a",} JSON の書き方が正しくありません
{"from":"2026/10/01"} from の形式が正しくありません(送られた値: 2026/10/01)
{"from":"2026-10-31","to":"2026-10-01"}(上の Range) from は to と同じか前の日付にします

返る本文の例です。

{
  "type": "about:blank",
  "title": "リクエストの本文を読み取れません",
  "status": 400,
  "detail": "items[1].qty の形式が正しくありません(送られた値: two)",
  "instance": "/api/orders",
  "field": "items[1].qty"
}

ex.getMessage() や Jackson の全文をそのまま返さないようにします。 クラス名(com.example.demo.…)や内部の構造が外に見えるためです。全文はログにだけ出します。

コードの要点

書き方 理由
ResponseEntityExceptionHandler を継承 Spring MVC の例外(415・405 など)もまとめて ProblemDetail になる
handleHttpMessageNotReadable を上書き 親クラスの @ExceptionHandler と重ならない(自分で同じ例外の @ExceptionHandler を書くと、Ambiguous @ExceptionHandler method mapped for … で起動に失敗する)
ex.getCause() を見る HttpMessageNotReadableException の原因に Jackson の例外が入っている
HttpMessageConversionException は 500 送る側ではなく、受け取るクラスの作りの誤り

設定だけで ProblemDetail にする

自分でハンドラーを書かない場合でも、次の設定で ProblemDetail 形式になります(Boot 4.1.1 の spring.mvc.problemdetails.enabled、既定は false)。

spring:
  mvc:
    problemdetails:
      enabled: true

このとき HttpMessageNotReadableException の detail は、Spring Framework 7.0.9 のソースでは Failed to read request という固定の文です。messages.properties に problemDetail.org.springframework.http.converter.HttpMessageNotReadableException=リクエストの本文を読み取れません と書くと、文を差し替えられます(Spring Framework のドキュメントの「Customization」)。自分で ResponseEntityExceptionHandler の Bean を作ると、Boot 側のハンドラーは作られません(@ConditionalOnMissingBean)。

例外処理の全体の設計は関連記事「Spring Boot例外処理|@ControllerAdvice完全版」で扱っています。エラー形式の決め方は関連記事「APIエラーレスポンス設計|RFC 9457準拠のJSON例と実装コード」で扱っています。

テストで確かめる(MockMvc)

@WebMvcTest(UserController.class)
@Import(ApiExceptionHandler.class)
class UserControllerTest {

    @Autowired MockMvc mvc;

    @Test
    void enumに無い値は400で項目名を返す() throws Exception {
        mvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name":"a","status":"DELETED"}
                    """))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.field").value("status"));
    }

    @Test
    void 本文が空なら400() throws Exception {
        mvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isBadRequest());
    }
}

テストの書き方は関連記事「Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け」で扱っています。

原因を絞り込む手順

  1. ログの JSON parse error: の後ろの書き出しを、冒頭の表で引く
  2. 項目が分からなければ、ex.getCause().getMessage() をログに出して reference chain を見る
  3. 送られた本文を見る(ブラウザの開発者ツール、または curl -v で送り直す)
  4. Boot 4 に上げた直後なら、record のプリミティブ型とJSON の後ろの余計な値を疑う
  5. Type definition error なら 500。送る側ではなく、受け取るクラス(抽象クラス・内部クラス)を直す
curl -i -X POST http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"山田","age":30}'

よくある質問

Q. Boot 3 から Boot 4 に上げたら、今まで通っていた JSON が 400 になりました。 A. ログが Cannot map `null` into type `int` なら、record やコンストラクタの int・boolean の項目が省略されています。Jackson 3 で既定が変わった点です。ラッパー型(Integer・Boolean)にするか、spring.jackson.deserialization.fail-on-null-for-primitives=false にします。

Q. ログに項目名が出ません。 A. Spring のログは Jackson の getOriginalMessage() を使うため、(through reference chain: …) の部分が出ません。上の @RestControllerAdvice のように ex.getCause().getMessage() をログに出すと、項目名とバイト位置が分かります。

Q. JsonParseException を catch するコードが Boot 4 でコンパイルできません。 A. Jackson 3 では tools.jackson.core.exc.StreamReadException です。型の誤りは tools.jackson.databind.exc.MismatchedInputException、全体の親は tools.jackson.core.JacksonException です。

Q. 本文をログに全部出して調べたいです。 A. 開発中だけにします。パスワードや個人情報が入ることがあるためです。フィルターで読むときは ContentCachingRequestWrapper で包み、読んだ後でもコントローラーが本文を受け取れるようにします。

まとめ

  • JSON parse error: の後ろは Jackson の文そのまま。書き出しで原因が決まる
  • Required request body is missing は本文が空。Spring が出す文
  • Boot 4 は Jackson 3(tools.jackson)。record のプリミティブ型の省略とJSON の後ろの余計な値が、新しくエラーになる
  • 日付は ISO 形式。それ以外は @JsonFormat(注釈は com.fasterxml.jackson.annotation のまま)
  • Type definition error は 500。受け取るクラスの作りを直す
  • 利用者には項目名と理由だけを ProblemDetail で返し、Jackson の全文はログへ

参考・出典

確認日はいずれも 2026年10月13日です。

  • Spring Framework リファレンス「Error Responses」:https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html
  • Spring Boot リファレンス「JSON」:https://docs.spring.io/spring-boot/reference/features/json.html
  • Spring Boot リファレンス「Servlet Web Applications」(Error Handling):https://docs.spring.io/spring-boot/reference/web/servlet.html
  • Spring Framework ソースコード(v7.0.9)AbstractJacksonHttpMessageConverter:https://github.com/spring-projects/spring-framework/blob/v7.0.9/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java
  • Spring Framework ソースコード(v7.0.9)RequestResponseBodyMethodProcessor:https://github.com/spring-projects/spring-framework/blob/v7.0.9/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
  • Spring Framework ソースコード(v7.0.9)ResponseEntityExceptionHandler:https://github.com/spring-projects/spring-framework/blob/v7.0.9/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
  • Spring Boot ソースコード(v4.1.1)JacksonProperties:https://github.com/spring-projects/spring-boot/blob/v4.1.1/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java
  • jackson-databind ソースコード(3.1.5)DeserializationFeature:https://github.com/FasterXML/jackson-databind/blob/jackson-databind-3.1.5/src/main/java/tools/jackson/databind/DeserializationFeature.java
  • RFC 8259 The JavaScript Object Notation (JSON) Data Interchange Format:https://www.rfc-editor.org/rfc/rfc8259
  • RFC 9457 Problem Details for HTTP APIs:https://www.rfc-editor.org/rfc/rfc9457

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「HttpMessageNotReadableExceptionの直し方|原因一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?