ProvenPin Install

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
pinned versions4
flask@3.1.3webargs@8.7.1marshmallow@4.3.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
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.

requirements.txt
flask==3.1.3
webargs==8.7.1
marshmallow==4.3.1
pytest==9.1.1
app.py
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
tests/test_api.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.