Handling HttpMessageNotReadableException in Spring Boot APIs
HttpMessageNotReadableException surfaces whenever Spring fails to translate an incoming request payload into a Java instance, most often via the @RequestBody mechanism. Typical triggers include:
- Empty Body: A controller method declares
@RequestBodybut the client omits the payload entirely. - Invalid Syntax: The payload contains broken JSON, such as missing quotes around string values or illegal trailing commas.
- Unsupported Content-Type: The
Content-Typeheader does not align with the payload structure, so no suitableHttpMessageConvertercan process the stream. - Field Type Mismatch: A JSON value cannot be maped to the target property type—for example,
"abc"supplied for anIntegerfield. - GET Requests with Body: Although technical permissible, GET requests rarely carry a body; applying
@RequestBodyto a GET handler usually results in this exception when the body is absent.
A global @RestControllerAdvice centralizes recovery:
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.HandlerMethod;
@RestControllerAdvice
public class RequestExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleParseFailure(HttpMessageNotReadableException ex, HandlerMethod handler) {
Throwable rootCause = ex.getRootCause();
if (rootCause instanceof InvalidFormatException invalidEx) {
String hint = TypeHintResolver.generateHint(invalidEx);
String detail = String.format("Payload parse failure: %s", ex.getMessage());
return ErrorResponse.failure(hint, "400", detail);
}
String clientMsg = "Request body could not be read. Please verify the data format.";
String logMsg = String.format("HttpMessageNotReadableException: %s", ex.getMessage());
return ErrorResponse.failure(clientMsg, "400", logMsg);
}
}
User-friendly hints for type errors improve API usability:
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import java.math.BigDecimal;
import java.math.BigInteger;
public class TypeHintResolver {
public static String generateHint(InvalidFormatException ex) {
Class<?> expected = ex.getTargetType();
String constraint = mapConstraint(expected);
Object received = ex.getValue();
return String.format("Invalid field format. %sInput received: [%s]", constraint, received);
}
private static String mapConstraint(Class<?> type) {
if (isIntegerFamily(type)) {
return "An integer value is required; ";
}
if (isFloatingPointFamily(type)) {
return "A numeric decimal is required; ";
}
return "";
}
private static boolean isIntegerFamily(Class<?> clazz) {
return clazz == Integer.class || clazz == Long.class || clazz == Short.class
|| clazz == Byte.class || clazz == BigInteger.class
|| clazz == int.class || clazz == long.class || clazz == short.class || clazz == byte.class;
}
private static boolean isFloatingPointFamily(Class<?> clazz) {
return clazz == BigDecimal.class || clazz == Double.class || clazz == Float.class
|| clazz == double.class || clazz == float.class;
}
}
The following endpoint accepts a member registration DTO:
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/members")
public class MemberController {
@PostMapping
public ResponseEntity<Void> register(@Valid @RequestBody MemberCreateDto request) {
return ResponseEntity.accepted().build();
}
}
The DTO defines the expected shape and types:
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class MemberCreateDto {
@NotBlank
private String fullName;
@Pattern(regexp = "^1[3-9]\\d{9}$")
private String contactPhone;
@Email
private String emailAddress;
private LocalDateTime activeFrom;
private LocalDateTime activeUntil;
@Min(18)
private Integer age;
@Max(100)
private Double rating;
}
Sending an empty body, passing "eighty" for age, or submitting malformed JSON such as an unquoted string value all trigger HttpMessageNotReadableException. The handler returns 400 Bad Request with a clear client message, ensuring that parse failures never expose internal stack traces while still preserving diagnostic details for logging.