- Spring
HttpMessageNotReadableExceptionの直し方|原因一覧
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の使い分け」で扱っています。
原因を絞り込む手順
- ログの
JSON parse error:の後ろの書き出しを、冒頭の表で引く - 項目が分からなければ、
ex.getCause().getMessage()をログに出してreference chainを見る - 送られた本文を見る(ブラウザの開発者ツール、または
curl -vで送り直す) - Boot 4 に上げた直後なら、record のプリミティブ型とJSON の後ろの余計な値を疑う
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期連続の黒字決算。