422 Unprocessable Entity 에러의 원인과 해결 방법|유효성 검사 에러를 올바르게 반환하는 방법
API를 개발할 때, 클라이언트에서 전송한 데이터가 구문상으로는 정확하지만 비즈니스 로직 요구사항을 충족하지 않는 경우가 자주 발생합니다. 이 경우 반환해야 할 HTTP 상태 코드는 <strong>422 Unprocessable Entity</strong>입니다. 이 문서에서는 422와 400의 차이, 주요 프레임워크의 구현 패턴, 파일 업로드에서 422의 활용 방법, 그리고 RFC 7807을 준수하는 에러 응답 설계까지 설명합니다.
422와 400의 차이: 구문 에러 vs 의미 에러
HTTP 400 (Bad Request)과 422 (Unprocessable Entity)는 혼동되기 쉬운 상태 코드이지만, 명확한 구분 기준이 있습니다.
| 상태 | 이름 | 의미 | 구체적인 예 |
|---|---|---|---|
| 400 | Bad Request | 요청 구문이 잘못되어 서버에서 해석할 수 없음 | 잘못된 JSON, 필수 헤더 누락, Content-Type 불일치 |
| 422 | Unprocessable Entity | 구문은 올바르지만 포함된 데이터의 의미가 부정확함 | 잘못된 이메일 형식, 범위를 벗어난 숫자 값, 잘못된 파일 형식 |
간단히 말해서, <strong>400은 파싱 에러</strong> (JSON이 손상되었다는 등), <strong>422는 검증 에러</strong> (JSON은 올바르지만 내용이 요구 사항을 충족하지 않음)입니다. 422는 원래 WebDAV 확장 (RFC 4918)에서 정의되었지만 현재는 REST API에서 널리 채택되고 있습니다.
// 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": ["有効なメールアドレスを入力してください。"]
}
}
Laravel에서 422 사용법
Laravel은 유효성 검사 실패 시 자동으로 422 응답을 반환합니다. 이는 <code>FormRequest</code> 또는 <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);
}
}
Laravel이 유효성 검사 실패 시 반환하는 응답은 요청이 JSON을 기대하는지(<code>Accept: application/json</code>) 여부에 따라 달라집니다. JSON 요청의 경우 422 상태와 함께 오류 JSON이 반환되고, 일반 양식 요청의 경우 세션에 오류를 저장한 후 원본 페이지로 리다이렉트됩니다.
Django에서 422 사용하기
Django REST Framework(DRF)는 기본적으로 유효성 검사 실패 시 400을 반환하지만, 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
Rails에서 422 사용 방법
Ruby on Rails에서는 모델 검증 실패 시 <code>:unprocessable_entity</code> 심볼로 422를 반환하는 것이 일반적입니다.
# 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
파일 업로드 기능에서는 다양한 검증으로 422를 반환하는 경우가 있습니다. 413(크기 초과)과 달리, 422는 서버가 요청을 정상적으로 수신했지만 파일 내용이 요건을 충족하지 않을 때 반환됩니다.
| 검증 항목 | 설명 | 스테이터스 코드 |
|---|---|---|
| 유효하지 않은 MIME 타입 | 확장자는 <code>.jpg</code>이지만 MIME 타입이 <code>text/plain</code>인 경우 | 422 |
| 잘못된 확장자 | 허용되지 않는 파일 형식(.exe 등) | 422 |
| 이미지 크기가 범위를 벗어남 | 최소 크기 미만 또는 최대 크기 초과 | 422 |
| 파일 손상 | 이미지로 로드할 수 없는 손상된 파일 | 422 |
| 바이러스 감지 | ClamAV 등에서 악성 코드가 감지된 경우 | 422 |
| 크기 초과 | 애플리케이션 측 제한 초과 | 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);
}
API의 422 응답 형식(RFC 7807 Problem Details)
API의 에러 응답을 표준화하기 위해 RFC 7807(Problem Details for HTTP APIs)이 제정되었습니다. 이 사양을 준수함으로써 클라이언트 측에서 에러 처리를 일관되게 수행할 수 있습니다.
// 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',
]);
}
}
프론트엔드 오류 표시 패턴
프론트엔드가 422 응답을 받으면 각 필드별로 에러 메시지를 사용자가 쉽게 이해할 수 있도록 표시해야 합니다.
// 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はインターセプターで処理済み
}
}
이 기사에서 사용할 수 있는 테스트 파일 (무료)
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">테스트 이미지 목록</a> — MIME 타입 및 파일 확장자 검증 테스트용
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">경계값 테스트 파일 목록</a> — 파일 크기 상한 검증의 경계값 테스트용
- → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">1MB 테스트 PNG 이미지</a> — 이미지 업로드 기본 테스트용
- → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF 테스트 파일 목록</a> — 파일 형식 유효성 검증 테스트용
관련 기사
- → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">413 Request Entity Too Large 에러의 원인과 해결방법</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">웹 폼 파일 검증 구현 체크리스트</a>
- → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Laravel 파일 업로드 구현 가이드|검증·Storage·S3 대응</a>
- → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">507 Insufficient Storage 에러의 원인과 해결방법</a>