python 117 lines · 3 tabs

Structured JSON Error Handling for Flask API Validation Failures

Shared by codesnips Aug 2026
3 tabs
from flask import jsonify
from marshmallow import ValidationError as MarshmallowValidationError
from werkzeug.exceptions import HTTPException


class ApiError(Exception):
    status = 400
    code = "error"

    def __init__(self, message, status=None, code=None, details=None):
        super().__init__(message)
        self.message = message
        if status is not None:
            self.status = status
        if code is not None:
            self.code = code
        self.details = details or {}

    def to_dict(self):
        return {
            "error": {
                "code": self.code,
                "message": self.message,
                "details": self.details,
            }
        }


class ValidationError(ApiError):
    status = 422
    code = "validation_error"


def register_error_handlers(app):
    @app.errorhandler(ApiError)
    def handle_api_error(err):
        return jsonify(err.to_dict()), err.status

    @app.errorhandler(MarshmallowValidationError)
    def handle_schema_error(err):
        payload = {
            "error": {
                "code": "validation_error",
                "message": "Request body failed validation.",
                "details": err.messages,
            }
        }
        return jsonify(payload), 422

    @app.errorhandler(HTTPException)
    def handle_http_error(err):
        payload = {
            "error": {
                "code": err.name.lower().replace(" ", "_"),
                "message": err.description,
                "details": {},
            }
        }
        return jsonify(payload), err.code
3 files · python Explain with highlit

This snippet shows how a Flask API turns messy exceptions into a single, predictable JSON envelope so clients never have to parse HTML error pages or guess at status codes. The core idea is to funnel every failure — application-level validation, schema validation, and generic HTTP errors — through one place, giving the whole service a consistent contract.

In errors.py, ApiError is a small base exception carrying a machine-readable code, a human message, an HTTP status, and optional details. ValidationError subclasses it and defaults to a 422 status with a validation_error code, which is the semantically correct response for a well-formed request that fails business rules. The to_dict method defines the wire format once, so every error response shares the same shape.

The register_error_handlers function is where the wiring happens. app.errorhandler(ApiError) catches any custom error raised deep in the call stack and serialises it with jsonify, setting the status from the exception itself. A second handler registered for marshmallow's MarshmallowValidationError adapts a third-party library's error format (err.messages, a dict of field to message lists) into the same envelope, so schema failures look identical to hand-raised ones. The final handler catches Werkzeug's HTTPException base class, which covers 404, 405, and friends, mapping each to the shared format via its code and name attributes. This layering matters: order of specificity is handled by Flask matching the most specific registered class, so ApiError wins over a plain Exception.

In app.py, create_app follows the application-factory pattern and calls register_error_handlers(app) during setup. The POST /users route loads and validates the body with UserSchema().load, and simply lets a raised MarshmallowValidationError propagate — no try/except clutter in the route. It also raises the custom ValidationError for a domain rule (duplicate email) that a schema can't express, showing how both paths converge on the same output.

The trade-off is that all responses now depend on the handlers being registered; a route that returns raw dicts on error would break the contract. The payoff is that clients get one parser, one code field to branch on, and reliable status codes. A common pitfall avoided here is letting HTTPException fall through to a generic 500 handler, which would strip the correct status. This pattern is worth reaching for the moment an API has more than a couple of endpoints or any non-trivial validation.


Related snips

Share this code

Here's the card — post it anywhere.

Structured JSON Error Handling for Flask API Validation Failures — share card
Link copied