Skip to content

Causes and Solutions for 422 Unprocessable Entity Errors | Proper Ways to Return Validation Errors

Category: HTTP / API Design
This article is currently available in Japanese only. We are working on translations.

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>