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 13:31 UTC- Status
- passed
- Last passed
- 2026-10-15 13:31 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 38742087302
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-SQLAlchemy models with Flask-Migrate upgrades on sqlite in tests
Define Flask-SQLAlchemy models, version the schema with Flask-Migrate, and test upgrade, downgrade and model/migration drift offline on a temporary sqlite file.
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.
All Flask recipesAll Flask-SQLAlchemy recipesAll marshmallow-sqlalchemy recipesAll marshmallow recipesAll SQLAlchemy recipes