Flask API args parsed and validated with webargs and marshmallow: working versions (tested 2026-10-15)
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.
passed5 passed · 0 failed2026-10-15 13:29 UTC- Status
- passed
- Last passed
- 2026-10-15 13:29 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("flask-marshmallow-webargs") (install), or see them on GitHub.
flask==3.1.3 webargs==8.7.1 marshmallow==4.3.1 pytest==9.1.1
from flask import Flask, abort, jsonify
from marshmallow import EXCLUDE, Schema, fields, validate
from webargs.flaskparser import FlaskParser
class JsonParser(FlaskParser):
# webargs' default 422 response is HTML; return the field errors as JSON
def handle_error(self, error, req, schema, *, error_status_code, error_headers):
resp = jsonify(errors=error.messages)
resp.status_code = error_status_code or 422
abort(resp)
parser = JsonParser()
class SearchArgs(Schema):
class Meta:
unknown = EXCLUDE # ignore stray query params instead of failing
q = fields.Str(required=True, validate=validate.Length(min=1))
page = fields.Int(load_default=1, validate=validate.Range(min=1))
tags = fields.List(fields.Str(), load_default=list) # ?tags=a&tags=b
include_archived = fields.Bool(load_default=False)
class ItemIn(Schema):
# default unknown=RAISE: extra JSON keys produce a 422
name = fields.Str(required=True)
qty = fields.Int(required=True, validate=validate.Range(min=0))
app = Flask(__name__)
@app.get("/search")
@parser.use_kwargs(SearchArgs(), location="query")
def search(q, page, tags, include_archived):
return jsonify(q=q, page=page, tags=tags, include_archived=include_archived)
@app.post("/items")
@parser.use_args(ItemIn(), location="json")
def create_item(data):
return jsonify(data), 201
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
return app.test_client()
def test_query_defaults_and_repeated_params(client):
r = client.get("/search?q=hi&tags=a&tags=b&include_archived=true")
assert r.status_code == 200
assert r.get_json() == {"q": "hi", "page": 1, "tags": ["a", "b"], "include_archived": True}
def test_missing_required_query_is_422_not_400(client):
r = client.get("/search")
assert r.status_code == 422
assert "q" in r.get_json()["errors"]["query"]
def test_range_validation_on_int_query(client):
r = client.get("/search?q=x&page=0")
assert r.status_code == 422
assert "page" in r.get_json()["errors"]["query"]
def test_unknown_json_key_is_rejected(client):
r = client.post("/items", json={"name": "bolt", "qty": 3, "color": "red"})
assert r.status_code == 422
assert "color" in r.get_json()["errors"]["json"]
def test_valid_json_creates_item(client):
r = client.post("/items", json={"name": "bolt", "qty": 3})
assert r.status_code == 201
assert r.get_json() == {"name": "bolt", "qty": 3}
[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 API with Flask-SQLAlchemy and marshmallow-sqlalchemy, validation errors as 422
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.
Flask 3 + Flask-JWT-Extended access and refresh tokens
Login, protected routes, refresh tokens and custom claims in Flask 3 using Flask-JWT-Extended, with an app factory.
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-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.