Python講座 / FlaskでJSON APIを作り入力を検証する

FlaskのAPIはHTTPリクエストをPython関数へ対応させます。JSONオブジェクト、必須項目、型と範囲を検査し、作成成功と不正入力をステータスコードで区別します。

ローカル開発の範囲を決める

このAPIは同じPC内の実習です。メモリー上のリストはプロセス終了で消え、独立したサーバープロセス間でも共有されません。公開サービスには永続化、認証、認可、配備と同時アクセスの設計が必要です。

プロジェクトの仮想環境へ導入します。

python -m pip install flask

app.pyとして保存してpython app.pyで起動します。5000番が使用中なら、ポートとクライアントのアドレスを一緒に変更します。

JSON表現と永続保存の違いは、JSONデータを変換して保存するで扱う規則とつながります。

APIアプリケーション全体を保存する

次をapp.pyへ保存します。

from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []

@app.errorhandler(413)
def too_large(error):
    return jsonify(error="request body too large"), 413

@app.get("/api/expenses")
def list_expenses():
    return jsonify(expenses)

@app.post("/api/expenses")
def add_expense():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return jsonify(error="expected JSON object"), 400
    name, amount = data.get("name"), data.get("amount")
    if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
        return jsonify(error="invalid name or amount"), 400
    row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
    expenses.append(row)
    return jsonify(row), 201

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

GET /api/expensesは一覧、POSTは一件作成です。成功は201、検証エラーは400を返します。MAX_CONTENT_LENGTHはリクエスト本文のサイズを制限しますが、アプリに必要なすべての資源制限を代行する設定ではありません。

テストクライアントで経路を確認する

同じフォルダーへtest_client.pyを保存します。

from app import app, expenses
expenses.clear()
with app.test_client() as client:
    created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
    assert created.status_code == 201
    for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
        response = client.post("/api/expenses", json=payload)
        assert response.status_code == 400 and response.is_json
    assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
    too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
    assert too_large.status_code == 413 and too_large.is_json
    rows = client.get("/api/expenses")
    assert rows.status_code == 200 and len(rows.get_json()) == 1
    print(created.status_code, rows.get_json()[0]["amount"])
print("validation and 413 checks passed")

実行します。

python test_client.py

予想結果は201 8000です。テストクライアントは別に起動したサーバーなしでFlaskの処理経路を呼びます。実際のネットワーク、リバースプロキシ、配備環境まで検証するものではありません。

型とアプリの寿命を確認する

JSONのtrueはboolになり、isinstance(True, int)はTrueです。そのため金額ではtypeによる厳密な整数検査で除外します。silent=Trueでは不正なJSONがNoneとなり、定義した入力エラーへ進みます。

開発サーバーはローカルに保ち、対話型デバッガーを公開しないでください。Ctrl+Cで止めて再起動すると一覧は消えます。永続化にはSQLiteなどの保存規則が必要です。リストの長さから作るIDも、この管理された実習用であり、同時利用する本番の識別と同期の代わりにはなりません。

関連する公式リファレンス

MAX_CONTENT_LENGTH超過は413で、Flaskの既定エラー画面はHTMLの場合があります。上の専用ハンドラーでJSONにそろえました。不正JSON・配列・bool金額・負数・空白名は400で保存しません。拡張テストの最後は validation and 413 checks passed です。この契約はアプリで扱う経路とエラーの範囲であり、前段のWebサーバーやプロキシが拒否した要求までは保証しません。

動作と適用条件の公式説明:Flask: handling application errors · Flask: MAX_CONTENT_LENGTH