Skip to content

Ursachen und Lösungen für Fehler 422 Unprocessable Entity | Korrekte Rückgabe von Validierungsfehlern

Kategorie:HTTP・API-Design
Dieser Artikel ist derzeit nur auf Japanisch verfügbar. Übersetzte Versionen werden schrittweise veröffentlicht.

Bei der Entwicklung einer API stoßen Sie häufig auf Fälle, in denen die vom Client gesendeten Daten syntaktisch korrekt sind, aber die geschäftslogischen Anforderungen nicht erfüllen. Der HTTP-Statuscode, der in diesem Fall zurückgegeben werden sollte, ist <strong>422 Unprocessable Entity</strong>. Dieser Artikel erklärt den Unterschied zwischen 422 und 400, Implementierungsmuster in gängigen Frameworks, wie man 422 bei Datei-Uploads nutzt und wie man Fehlerantworten im Einklang mit RFC 7807 gestaltet.

Unterschied zwischen 422 und 400: Syntaxfehler vs semantischer Fehler

HTTP 400 (Bad Request) und 422 (Unprocessable Entity) sind leicht verwechselbare Statuscodes, aber es gibt klare Unterscheidungskriterien.

Status Name Bedeutung Konkretes Beispiel
400 Bad Request Die Anfrage-Syntax ist ungültig und der Server kann sie nicht analysieren Ungültiges JSON, fehlender erforderlicher Header, Content-Type-Fehlanpassung
422 Unprocessable Entity Die Syntax ist korrekt, aber die Bedeutung der enthaltenen Daten ist ungültig Ungültiges E-Mail-Adressformat, Werte außerhalb des Bereichs, ungültiges Dateiformat

Kurz gesagt: <strong>400 ist ein Parse-Fehler</strong> (beschädigtes JSON usw.), <strong>422 ist ein Validierungsfehler</strong> (JSON ist korrekt, aber der Inhalt erfüllt die Anforderungen nicht). 422 wurde ursprünglich in der WebDAV-Erweiterung (RFC 4918) definiert, wird aber heute weit verbreitet in REST-APIs verwendet.

// 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": ["有効なメールアドレスを入力してください。"]
    }
}

So verwenden Sie den Status 422 in Laravel

Laravel gibt automatisch eine Antwort mit dem Status 422 zurück, wenn die Validierung fehlschlägt. Dies ist das Standardverhalten bei Verwendung von <code>FormRequest</code> oder <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);
    }
}

Die von Laravel zurückgegebene Antwort bei einem Validierungsfehler hängt davon ab, ob die Anfrage JSON erwartet (<code>Accept: application/json</code>) oder nicht. Bei JSON-Anfragen wird ein Fehler-JSON mit dem Status 422 zurückgegeben, während bei normalen Formularanfragen der Fehler in der Sitzung gespeichert und zurück zur ursprünglichen Seite umgeleitet wird.

So verwenden Sie 422 in Django

Django REST Framework (DRF) gibt standardmäßig 400 bei Validierungsfehlern zurück, kann aber angepasst werden, um stattdessen 422 zu verwenden.

# 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

Verwendung von 422 Status Code in Rails

In Ruby on Rails ist es üblich, 422 mit dem Symbol <code>:unprocessable_entity</code> zurückzugeben, wenn die Validierung des Modells fehlschlägt.

# 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 beim Datei-Upload

Bei der Datei-Upload-Funktion gibt es Fälle, in denen 422 durch verschiedene Validierungen zurückgegeben wird. Im Gegensatz zu 413 (Größe überschritten) wird 422 zurückgegeben, wenn der Server die Anfrage normal empfängt, aber der Inhalt der Datei die Anforderungen nicht erfüllt.

Validierungselemente Beschreibung Statuscode
Ungültiger MIME-Typ Wenn die Dateityp .jpg ist, aber der MIME-Typ text/plain ist 422
Ungültiger Dateityp Nicht zulässiges Dateiformat (wie .exe) 422
Bildabmessungen außerhalb des Bereichs Unter Minimumgröße, über Maximumgröße 422
Dateikorruption Beschädigte Datei, die nicht als Bild geladen werden kann 422
Viruserkennung Wenn Malware von ClamAV usw. erkannt wird 422
Größe überschritten Anwendungsseitiges Limit überschritten 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-Antwortformat in APIs (RFC 7807 Problem Details)

RFC 7807 (Problem Details for HTTP APIs) wurde festgelegt, um die Fehlerantworten von APIs zu standardisieren. Durch die Einhaltung dieser Spezifikation kann die Fehlerbehandlung auf der Client-Seite einheitlich durchgeführt werden.

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

Fehlermeldungsmuster im Front-End

Wenn das Frontend eine 422-Antwort erhält, muss es Fehlermeldungen für jedes Feld klar und verständlich für den Benutzer anzeigen.

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

Testdatei zur Verwendung in diesem Artikel (kostenlos)

  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Liste der Testbilder</a> — Zum Testen der MIME-Typ- und Dateierweiterungsvalidierung
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Liste von Grenzwert-Testdateien</a> — Für Grenzwert-Tests der Größenobergrenze-Validierung
  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">1MB Test-PNG-Bild</a> — Zum Basis-Test des Bild-Uploads
  • → <a href="/ja/files/pdf/" class="text-primary-600 dark:text-primary-400 hover:underline">Liste der PDF-Testdateien</a> — Zum Testen der Dateiformat-Validierung

Verwandte Artikel

  • → <a href="/ja/blog/http-413-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Ursachen und Lösungen für den Fehler 413 Request Entity Too Large</a>
  • → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Checkliste zur Implementierung der Dateiverifizierung für Web-Formulare</a>
  • → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Implementierungsleitfaden für Datei-Uploads in Laravel | Validierung, Storage und S3-Unterstützung</a>
  • → <a href="/ja/blog/http-507-error/" class="text-primary-600 dark:text-primary-400 hover:underline">Ursachen und Lösungen für den Fehler 507 Insufficient Storage</a>