Flask API with Flask-SQLAlchemy and marshmallow-sqlalchemy, validation errors as 422: working versions (tested 2026-10-15)
A Flask JSON API that loads and dumps SQLAlchemy models through SQLAlchemyAutoSchema, returns validation errors as 422, and is tested offline with the Flask test client on in-memory sqlite.
passed5 passed · 0 failed2026-10-15 11:55 UTC- Status
- passed
- Last passed
- 2026-10-15 11:55 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("marshmallow-sqlalchemy-flask-api") (install), or see them on GitHub.
flask==3.1.3 flask-sqlalchemy==3.1.1 marshmallow==3.26.1 marshmallow-sqlalchemy==1.5.0 sqlalchemy==2.0.36 pytest==9.1.1
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from marshmallow import ValidationError
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from sqlalchemy import String
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Mapped, mapped_column
db = SQLAlchemy()
class Book(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(120))
isbn: Mapped[str] = mapped_column(String(13), unique=True)
class BookSchema(SQLAlchemyAutoSchema):
class Meta:
model = Book
load_instance = True
sqla_session = db.session
dump_only = ("id",)
def create_app(uri: str = "sqlite:///:memory:") -> Flask:
app = Flask(__name__)
app.config.update(SQLALCHEMY_DATABASE_URI=uri)
db.init_app(app)
@app.post("/books")
def create_book():
try:
book = BookSchema().load(request.get_json(silent=True) or {})
except ValidationError as err:
return {"errors": err.messages}, 422
db.session.add(book)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
return {"errors": {"isbn": ["already exists"]}}, 409
return BookSchema().dump(book), 201
return app
import pytest
from app import create_app, db
@pytest.fixture
def client():
app = create_app()
app.config["TESTING"] = True
with app.app_context():
db.create_all()
yield app.test_client()
db.session.remove()
db.drop_all()
def test_create_returns_201_and_dumps_id(client):
r = client.post("/books", json={"title": "Dune", "isbn": "9780441013593"})
assert r.status_code == 201
body = r.get_json()
assert body["id"] == 1
assert body["title"] == "Dune"
def test_missing_required_field_is_422(client):
r = client.post("/books", json={"isbn": "9780441013593"})
assert r.status_code == 422
assert "title" in r.get_json()["errors"]
def test_title_length_comes_from_column_type_422(client):
# marshmallow-sqlalchemy derives a Length validator from String(120)
r = client.post("/books", json={"title": "x" * 121, "isbn": "9780441013593"})
assert r.status_code == 422
assert "title" in r.get_json()["errors"]
def test_unknown_field_is_422(client):
# marshmallow 3 defaults to unknown=RAISE, so extra keys fail validation
r = client.post("/books", json={"title": "Dune", "isbn": "9780441013593", "pages": 412})
assert r.status_code == 422
assert "pages" in r.get_json()["errors"]
def test_duplicate_isbn_is_409_and_session_recovers(client):
client.post("/books", json={"title": "Dune", "isbn": "9780441013593"})
r = client.post("/books", json={"title": "Dune again", "isbn": "9780441013593"})
assert r.status_code == 409
# the session was rolled back, so a fresh insert still works
r = client.post("/books", json={"title": "Emma", "isbn": "9780141439587"})
assert r.status_code == 201
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Flask 3 app factory + Flask-SQLAlchemy
Flask application factory with Flask-SQLAlchemy 3, a JSON API and pytest fixtures using an in-memory database.
Flask API args parsed and validated with webargs and marshmallow
Flask routes that parse query strings and JSON bodies through marshmallow schemas via webargs, with JSON 422 validation errors, tested offline with the Flask test client.
Flask-Login sessions with Flask-SQLAlchemy users and werkzeug password hashing
Login, login_required redirects, logout and a user_loader built on db.session.get, with the User model in SQLite memory and tests run through the Flask test client.
Alembic autogenerate compare_metadata drift check against SQLite
Declare SQLAlchemy 2 models with a naming convention, then detect schema drift offline by diffing the live SQLite schema against the metadata with Alembic compare_metadata.
All Flask recipesAll Flask-SQLAlchemy recipesAll marshmallow-sqlalchemy recipesAll marshmallow recipesAll SQLAlchemy recipes