Skip to content

Causas e soluções para o erro 422 Unprocessable Entity | Como retornar corretamente erros de validação

Categoria:Design de HTTP・API
Este artigo está disponível atualmente apenas em japonês. As versões traduzidas serão publicadas sequencialmente.

Ao desenvolver uma API, você frequentemente encontra casos em que os dados enviados pelo cliente estão sintaticamente corretos, mas não atendem aos requisitos da lógica de negócios. O código de status HTTP que deve ser retornado neste caso é <strong>422 Unprocessable Entity</strong>. Este artigo explica a diferença entre 422 e 400, padrões de implementação em estruturas principais, como usar 422 em uploads de arquivo e como projetar respostas de erro em conformidade com RFC 7807.

Diferença entre 422 e 400: erro de sintaxe vs erro de semântica

HTTP 400 (Bad Request) e 422 (Unprocessable Entity) são códigos de status facilmente confundidos, mas possuem critérios de uso bem definidos.

Status Nome Significado Exemplo concreto
400 Bad Request A sintaxe da solicitação é inválida e o servidor não consegue analisá-la JSON inválido, cabeçalho obrigatório ausente, incompatibilidade de Content-Type
422 Unprocessable Entity A sintaxe está correta, mas o significado dos dados contidos é inválido Formato de endereço de email inválido, valores fora do intervalo, formato de arquivo inválido

Em poucas palavras, <strong>400 é erro de análise</strong> (JSON corrompido, etc.), <strong>422 é erro de validação</strong> (JSON está correto, mas o conteúdo não atende aos requisitos). 422 foi originalmente definido na extensão WebDAV (RFC 4918), mas agora é amplamente adotado em 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": ["有効なメールアドレスを入力してください。"]
    }
}

Como usar o código 422 no Laravel

Laravel retorna automaticamente uma resposta 422 quando a validação falha. Este é o comportamento padrão ao usar <code>FormRequest</code> ou <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);
    }
}

A resposta retornada pelo Laravel quando a validação falha varia dependendo se a requisição espera JSON (<code>Accept: application/json</code>) ou não. Para requisições JSON, um JSON de erro é retornado com o status 422, enquanto para requisições de formulário comuns, o erro é armazenado na sessão e redireciona para a página anterior.

Como usar 422 no Django

Django REST Framework (DRF) retorna 400 por padrão quando a validação falha, mas pode ser customizado 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

Como usar o status 422 no Rails

No Ruby on Rails, é comum retornar 422 com o símbolo <code>:unprocessable_entity</code> quando a validação do modelo falha.

# 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 em upload de arquivo

Na funcionalidade de upload de arquivo, há casos em que 422 é retornado por vários tipos de validação. Diferentemente de 413 (tamanho excedido), 422 é retornado quando o servidor recebe a requisição normalmente, mas o conteúdo do arquivo não atende aos requisitos.

Itens de validação Descrição Código de status
Tipo MIME inválido Quando a extensão é .jpg, mas o tipo MIME é text/plain 422
Extensão inválida Formato de arquivo não permitido (como .exe) 422
Dimensões de imagem fora do intervalo Abaixo do tamanho mínimo, acima do tamanho máximo 422
Corrupção de arquivo Arquivo corrompido que não pode ser carregado como imagem 422
Detecção de vírus Quando malware é detectado por ClamAV ou similares 422
Tamanho excedido Limite do lado da aplicação excedido 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 Resposta 422 em APIs (RFC 7807 Problem Details)

O RFC 7807 (Problem Details for HTTP APIs) foi estabelecido para padronizar respostas de erro em APIs. Ao estar em conformidade com essa especificação, o tratamento de erros no lado do cliente pode ser feito de forma unificada.

// 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',
        ]);
    }
}

Padrões de exibição de erros no front-end

Ao receber uma resposta 422, o frontend deve exibir mensagens de erro para cada campo de forma clara e compreensível ao usuário.

// 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はインターセプターで処理済み
    }
}

Arquivo de teste disponível para usar neste artigo (gratuito)

  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de imagens de teste</a> — Para teste de validação de tipo MIME e extensão de arquivo
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de arquivos de teste de valores limite</a> — Para testes de valores limite de validação de limite de tamanho
  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Imagem PNG de teste 1MB</a> — Para teste básico de upload de imagem
  • → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de arquivos de teste PDF</a> — Para teste de validação de formato de arquivo

Artigos relacionados

  • → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Causas e soluções do erro 413 Request Entity Too Large</a>
  • → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de verificação de implementação de validação de arquivo para formulários web</a>
  • → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Guia de implementação de upload de arquivos no Laravel | Validação, Storage e suporte S3</a>
  • → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Causas e soluções do erro 507 Insufficient Storage</a>