Causes and Solutions for 422 Unprocessable Entity Errors | Proper Ways to Return Validation Errors
When developing APIs, you frequently encounter cases where data sent from the client is syntactically correct but does not meet business logic requirements. The HTTP status code to return in this case is <strong>422 Unprocessable Entity</strong>. This article explains the differences between 422 and 400, implementation patterns in major frameworks, how to use 422 in file uploads, and how to design error responses compliant with RFC 7807.
Difference Between 422 and 400: Syntax Error vs Semantic Error
HTTP 400 (Bad Request) and 422 (Unprocessable Entity) are often confused, but there are clear criteria for distinguishing them.
| Status | Name | Meaning | Concrete example |
|---|---|---|---|
| 400 | Bad Request | The request syntax is invalid and the server cannot parse it | Invalid JSON, missing required headers, Content-Type mismatch |
| 422 | Unprocessable Entity | Syntax is correct but the meaning of contained data is invalid | Invalid email format, out-of-range numeric values, or invalid file format |
In short, <strong>400 is a parse error</strong> (malformed JSON, etc.), while <strong>422 is a validation error</strong> (JSON is correct but content does not meet requirements). 422 was originally defined in the WebDAV extension (RFC 4918), but is now widely adopted in REST APIs.
// 400 Bad Request の例: JSONの構文が壊れている
// リクエストボディ: {"name": "太郎", "email": } ← JSONパースエラー
// 422 Unprocessable Entity の例: JSONは正しいがバリデーション失敗
// リクエストボディ: {"name": "", "email": "not-an-email"}
// レスポンス:
{
"message": "The given data was invalid.",
"errors": {
"name": ["名前は必須です。"],
"email": ["有効なメールアドレスを入力してください。"]
}
}
How to Use 422 in Laravel
Laravel automatically returns a 422 response when validation fails. This is the default behavior when using <code>FormRequest</code> or <code>$request->validate()</code>.
// Laravel: バリデーション失敗時に自動で422を返す
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'age' => ['required', 'integer', 'min:0', 'max:150'],
'avatar' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:5120'],
];
}
public function messages(): array
{
return [
'name.required' => '名前は必須です。',
'email.required' => 'メールアドレスは必須です。',
'email.email' => '有効なメールアドレスを入力してください。',
'email.unique' => 'このメールアドレスは既に登録されています。',
'avatar.max' => 'アバター画像は5MB以下にしてください。',
];
}
}
// コントローラー
class UserController extends Controller
{
public function store(StoreUserRequest $request)
{
// バリデーション通過済み - ここに到達した時点で422は返らない
$user = User::create($request->validated());
return response()->json($user, 201);
}
}
The response that Laravel returns when validation fails depends on whether the request expects JSON (<code>Accept: application/json</code>). For JSON requests, an error JSON is returned with a 422 status, while for regular form requests, the error is stored in the session and the user is redirected to the original page.
How to Use 422 in Django
Django REST Framework (DRF) returns 400 by default on validation failure, but it can be customized to use 422.
# Django REST Framework: カスタム例外ハンドラーで422を返す
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError
from rest_framework import status
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if isinstance(exc, ValidationError) and response is not None:
# バリデーションエラーのステータスを422に変更
response.status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
return response
# settings.py に設定
# REST_FRAMEWORK = {
# 'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
# }
# シリアライザーでのバリデーション
from rest_framework import serializers
class UserSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
email = serializers.EmailField()
avatar = serializers.ImageField(required=False)
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError(
"このメールアドレスは既に登録されています。"
)
return value
How to use 422 in Rails
In Ruby on Rails, it's common to return 422 using the <code>:unprocessable_entity</code> symbol when model validation fails.
# Rails: バリデーションエラー時に422を返す
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
render json: user, status: :created
else
render json: {
message: "バリデーションエラー",
errors: user.errors.full_messages
}, status: :unprocessable_entity # 422
end
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar)
end
end
# モデルのバリデーション
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 255 }
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :email, uniqueness: true
has_one_attached :avatar
validates :avatar, content_type: ['image/jpeg', 'image/png', 'image/webp'],
size: { less_than: 5.megabytes }
end
422 in File Upload
File upload functionality may return 422 for various validation failures. Unlike 413 (size exceeded), 422 is returned when the server successfully receives the request but the file content does not meet requirements.
| Validation Item | Description | Status code |
|---|---|---|
| Invalid MIME Type | Case where extension is <code>.jpg</code> but MIME type is <code>text/plain</code> | 422 |
| Invalid Extension | File format not permitted (such as .exe) | 422 |
| Image dimensions out of range | Below minimum size or exceeding maximum size | 422 |
| File Corruption | Corrupted file that cannot be loaded as an image | 422 |
| Virus detection | When malware is detected by ClamAV or similar tools | 422 |
| Size exceeded | Application-side limit exceeded | 422 or 413 |
// ファイルアップロードの詳細バリデーション例(Laravel)
public function uploadAvatar(Request $request)
{
$request->validate([
'avatar' => ['required', 'file'],
]);
$file = $request->file('avatar');
// MIMEタイプの二重チェック(拡張子偽装対策)
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
$detectedMime = $file->getMimeType(); // finfo による判定
if (!in_array($detectedMime, $allowedMimes)) {
return response()->json([
'message' => 'バリデーションエラー',
'errors' => [
'avatar' => [
"許可されていないファイル形式です。検出されたMIMEタイプ: {$detectedMime}"
]
]
], 422);
}
// 画像として正常に読み込めるか確認
$imageInfo = @getimagesize($file->getRealPath());
if ($imageInfo === false) {
return response()->json([
'message' => 'バリデーションエラー',
'errors' => [
'avatar' => ['ファイルが破損しているか、有効な画像ファイルではありません。']
]
], 422);
}
// 画像寸法チェック
[$width, $height] = $imageInfo;
if ($width < 100 || $height < 100) {
return response()->json([
'message' => 'バリデーションエラー',
'errors' => [
'avatar' => ["画像は100x100px以上である必要があります。現在: {$width}x{$height}px"]
]
], 422);
}
// バリデーション通過 - 保存処理
$path = $file->store('avatars', 'public');
return response()->json(['path' => $path], 201);
}
422 Response Format for APIs (RFC 7807 Problem Details)
RFC 7807 (Problem Details for HTTP APIs) has been established to standardize error responses in APIs. By adhering to this specification, error handling on the client side can be performed consistently.
// RFC 7807 準拠の 422 レスポンス例
// Content-Type: application/problem+json
{
"type": "https://example.com/problems/validation-error",
"title": "バリデーションエラー",
"status": 422,
"detail": "送信されたデータに2件のエラーがあります。",
"instance": "/api/users",
"errors": [
{
"field": "email",
"message": "有効なメールアドレスを入力してください。",
"code": "invalid_format"
},
{
"field": "avatar",
"message": "ファイル形式はJPEG・PNG・WebPのみ対応しています。",
"code": "invalid_mime_type"
}
]
}
// Laravel で RFC 7807 準拠のレスポンスを返す
use Symfony\Component\HttpFoundation\Response;
class ApiController extends Controller
{
protected function validationProblem(
array $errors,
string $detail = 'バリデーションエラーが発生しました。'
): Response {
$formattedErrors = [];
foreach ($errors as $field => $messages) {
foreach ($messages as $message) {
$formattedErrors[] = [
'field' => $field,
'message' => $message,
];
}
}
return response()->json([
'type' => 'https://example.com/problems/validation-error',
'title' => 'Unprocessable Entity',
'status' => 422,
'detail' => $detail,
'errors' => $formattedErrors,
], 422, [
'Content-Type' => 'application/problem+json',
]);
}
}
Front-end error display patterns
When the frontend receives a 422 response, it must display error messages for each field in a clear and user-friendly manner.
// fetch API での 422 エラーハンドリング
async function submitForm(formData) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (response.status === 422) {
const data = await response.json();
// フィールドごとにエラーを表示
clearErrors();
for (const [field, messages] of Object.entries(data.errors)) {
const input = document.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('border-red-500');
const errorDiv = document.createElement('div');
errorDiv.className = 'text-red-500 text-sm mt-1';
errorDiv.textContent = messages[0];
input.parentNode.appendChild(errorDiv);
}
}
return;
}
if (!response.ok) {
throw new Error('サーバーエラーが発生しました。');
}
const result = await response.json();
showSuccess('登録が完了しました。');
} catch (error) {
showError(error.message);
}
}
function clearErrors() {
document.querySelectorAll('.border-red-500').forEach(el => {
el.classList.remove('border-red-500');
});
document.querySelectorAll('.text-red-500').forEach(el => {
el.remove();
});
}
// axios での 422 エラーハンドリング(Vue.js / React 等で利用)
import axios from 'axios';
// グローバルインターセプターで422を処理
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 422) {
// バリデーションエラーをストアに保存
const errors = error.response.data.errors;
store.commit('setValidationErrors', errors);
}
return Promise.reject(error);
}
);
// コンポーネントでの使用例
async function handleSubmit() {
try {
store.commit('clearValidationErrors');
const response = await axios.post('/api/users', formData);
// 成功処理
} catch (error) {
if (error.response?.status !== 422) {
// 422以外のエラーはグローバルで処理
alert('予期しないエラーが発生しました。');
}
// 422はインターセプターで処理済み
}
}
Test files for this article (free)
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Test Images List</a> — For testing MIME type and file extension validation
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Boundary Value Test Files</a> — For boundary value testing of file size limit validation
- → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">1MB Test PNG Image</a> — For basic image upload testing
- → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF Test File List</a> — for file format validation testing
Related articles
- → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">413 Request Entity Too Large Error: Causes and Solutions</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Web Form File Validation Implementation Checklist</a>
- → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Laravel File Upload Implementation Guide | Validation, Storage, and S3 Support</a>
- → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">507 Insufficient Storage Error: Causes and Troubleshooting</a>