Causas y soluciones de errores 422 Unprocessable Entity | Formas correctas de retornar errores de validación
Al desarrollar API, con frecuencia se encuentran casos donde los datos enviados desde el cliente son sintácticamente correctos pero no cumplen con los requisitos de la lógica comercial. El código de estado HTTP a devolver en este caso es <strong>422 Unprocessable Entity</strong>. Este artículo explica las diferencias entre 422 y 400, patrones de implementación en marcos principales, cómo usar 422 en carga de archivos y cómo diseñar respuestas de error conformes con RFC 7807.
Diferencia entre 422 y 400: Error de sintaxis vs Error semántico
HTTP 400 (Bad Request) y 422 (Unprocessable Entity) son códigos de estado que se confunden fácilmente, pero hay criterios claros para distinguirlos.
| Estado | Nombre | Significado | Ejemplo concreto |
|---|---|---|---|
| 400 | Bad Request | La sintaxis de la solicitud es inválida y el servidor no puede analizarla | JSON inválido, falta de encabezados requeridos, falta de coincidencia de Content-Type |
| 422 | Unprocessable Entity | La sintaxis es correcta pero el significado de los datos contenidos es inválido | Formato de correo electrónico inválido, valores numéricos fuera de rango o formato de archivo inválido |
En pocas palabras, <strong>400 es un error de análisis</strong> (JSON malformado, etc.), mientras que <strong>422 es un error de validación</strong> (JSON es correcto pero el contenido no cumple requisitos). 422 fue originalmente definido en la extensión WebDAV (RFC 4918), pero ahora es ampliamente adoptado en APIs REST.
// 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": ["有効なメールアドレスを入力してください。"]
}
}
Cómo usar 422 en Laravel
Laravel devuelve automáticamente una respuesta 422 cuando falla la validación. Este es el comportamiento predeterminado al usar <code>FormRequest</code> o <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);
}
}
La respuesta que Laravel devuelve cuando falla la validación depende de si la solicitud espera JSON (<code>Accept: application/json</code>). Para solicitudes JSON, se devuelve un JSON de error con estado 422, mientras que para solicitudes de formulario normales, el error se almacena en la sesión y se redirige al usuario a la página original.
Cómo usar 422 en Django
Django REST Framework (DRF) devuelve 400 por defecto en caso de fallo de validación, pero puede personalizarse para usar 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
Cómo usar 422 en Rails
En Ruby on Rails, es común devolver 422 usando el símbolo <code>:unprocessable_entity</code> cuando la validación del modelo falla.
# 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 en carga de archivos
La funcionalidad de carga de archivos puede devolver 422 para diversas fallas de validación. A diferencia de 413 (tamaño excedido), 422 se devuelve cuando el servidor recibe correctamente la solicitud pero el contenido del archivo no cumple con los requisitos.
| Elemento de validación | Descripción | Código de estado |
|---|---|---|
| Tipo MIME inválido | Caso en el que la extensión es <code>.jpg</code> pero el tipo MIME es <code>text/plain</code> | 422 |
| Extensión Inválida | Formato de archivo no permitido (como .exe) | 422 |
| Dimensiones de imagen fuera de rango | Por debajo del tamaño mínimo o excede el tamaño máximo | 422 |
| Corrupción de archivo | Archivo corrupto que no se puede cargar como imagen | 422 |
| Detección de virus | Cuando se detecta malware mediante ClamAV u herramientas similares | 422 |
| Tamaño excedido | Límite del lado de la aplicación superado | 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);
}
Formato de respuesta 422 para API (RFC 7807 Problem Details)
Se ha establecido RFC 7807 (Detalles de problemas para API HTTP) para estandarizar las respuestas de error en las API. Al adherirse a esta especificación, el manejo de errores en el lado del cliente puede realizarse de manera consistente.
// 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',
]);
}
}
Patrones de visualización de errores en front-end
Cuando el frontend recibe una respuesta 422, debe mostrar mensajes de error para cada campo de forma clara y amigable para el usuario.
// 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はインターセプターで処理済み
}
}
Archivos de prueba para este artículo (gratis)
- → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de imágenes de prueba</a> — Para probar validación de tipo MIME y extensión de archivo
- → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de archivos de prueba de valores límite</a> — Para pruebas de valores límite de validación de tamaño de archivo
- → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Imagen PNG de Prueba 1MB</a> — Para pruebas básicas de carga de imágenes
- → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de archivos de prueba PDF</a> — para pruebas de validación de formato de archivo
Artículos relacionados
- → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Error 413 Request Entity Too Large: Causas y Soluciones</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de Verificación de Implementación de Validación de Archivos en Formularios Web</a>
- → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Guía de Implementación de Carga de Archivos en Laravel | Validación, Storage y S3</a>
- → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Error 507 Insufficient Storage: Causas y Soluciones</a>