L09 ch02 — JSON ファイル永続化
テクノロジ系 / L09 file-io — ファイル永続化
L09 ch02 — JSON ファイル永続化
メモリ上のデータをファイルに残し、再起動後も使う
ゴール
json.dumpでファイル書き込みjson.loadでファイル読み込み- API データの永続化パターン
書き込み
import json
tasks = [
{"id": 1, "title": "買い物", "done": False},
{"id": 2, "title": "掃除", "done": True},
]
with open("tasks.json", "w", encoding="utf-8") as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)読み込み
with open("tasks.json", "r", encoding="utf-8") as f:
tasks = json.load(f)
print(tasks)
# [{'id': 1, 'title': '買い物', ...}, ...]ensure_ascii=False が重要
# ❌ デフォルト
json.dump({"name": "太郎"}, f)
# → {"name": "太郎"}
# ✅ 日本語そのまま
json.dump({"name": "太郎"}, f, ensure_ascii=False)
# → {"name": "太郎"}load/save パターン
DB_FILE = Path("tasks.json")
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.dumps(tasks, ensure_ascii=False, indent=2),
encoding="utf-8",
)FastAPI と組み合わせ
@app.get("/tasks")
def get_tasks():
return load()
@app.post("/tasks")
async def create(req: Request):
body = await req.json()
tasks = load()
tasks.append({"id": len(tasks)+1, **body})
save(tasks)
return tasks[-1]罠
- 同時書き込み競合 → ロックが必要 (本格運用は DB へ)
- 大きな JSON は読込時にメモリ展開 (1GB+ なら ndjson)
- ファイル破損対策: 一旦
.tmpに書いて rename
drill
drill/ — JSON 永続化付き CRUD
ハンズオン
ch02-json 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の構造は そのまま
- ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しいエンドポイント/機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する I/F (URL・HTTPメソッド・レスポンスのキー名) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
main.pyから 自力で全部書く - ファイル構造・関数分割・命名・内部ロジック: 完全に自由
- 守るのはインターフェースだけ: URL・HTTP メソッド・レスポンス JSON 形式
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
main.py) は変えない tests/フォルダは触らない (採点ロジック)
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-load-save | json.load / json.dump - ファイル読み書き | 🔧 穴埋め | ★ |
| 2 | 02-add-save | json.dump - JSON ファイルへの保存 | ➕ 機能追加 | ★ |
| 3 | 03-add-query-filter | JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ | ➕ 機能追加 | ★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: json.load / json.dump - ファイル読み書き
ドリル名: 01-fill-load-save
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を3箇所埋めて、JSONファイルへの読み書きを完成させよ。
def load() -> list[dict]:
if not DB_FILE.___(): # ファイルの存在確認メソッドを埋める
return []
return json.___(DB_FILE.read_text(encoding="utf-8")) # JSON文字列→Pythonオブジェクト変換を埋める
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.___(tasks, ensure_ascii=False, indent=2), # Pythonオブジェクト→JSON文字列変換を埋める
encoding="utf-8"
)確認方法
POST /tasks→{"title": "牛乳を買う"}を送信する- カレントディレクトリに
tasks.jsonが作成されること GET /tasksでタスク一覧が返ること
ドリル 1: ヒント
ドリル名: 01-fill-load-save
> ⚠️ 自力で考えてから読んでください
> - pathlib.Path にはファイルが存在するかどうかを確認するメソッドがある。辞書の in チェックのような使い方で、真偽値を返す。
> - json モジュールには文字列とPythonオブジェクトを相互変換する関数が2つある。
> - 文字列 → Python オブジェクト: json.loads(s は string の s)
> - Python オブジェクト → 文字列: json.dumps(s は同じく string の s)
ドリル 2: json.dump - JSON ファイルへの保存
ドリル名: 02-add-save
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
問題
base/main.py のメモリ実装を、JSONファイル永続化に書き直せ。
仕様:
jsonとpathlib.PathをインポートするDB_FILE = Path("tasks.json")を定義するload()/save()関数を追加する- 各エンドポイントでメモリ変数
tasksの代わりにload()を使う - 変更があるエンドポイントは変更後に
save()を呼ぶ global next_idは不要になる。ID採番はlen(tasks) + 1で行う
確認方法
POST /tasks→{"title": "牛乳を買う"}を送信する- サーバーを
Ctrl+Cで停止し、再起動する GET /tasksでタスクが残っていること
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
save()内でtasks.json.bakというバックアップファイルも一緒に書き出して、tasks.jsonが壊れたとき復旧できるようにする- ID 採番を
len(tasks) + 1から「現在保持している最大 ID + 1」に変更し、削除後に追加しても ID が衝突しないようにする load()に簡易ログ (print(f"loaded {len(tasks)} tasks")) を入れて、起動時にどれだけ読み込んだか確認する
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-save
> ⚠️ 自力で考えてから読んでください
> - ファイルの先頭に import json と from pathlib import Path を追加する。
> - DB_FILE = Path("tasks.json") をアプリ直下(app = FastAPI() の近く)に定義する。
> - load() / save() の実装は fill-load-save で埋めたものと同じ。
> - 各エンドポイントの書き換えパターンは共通:
> 1. 関数の先頭で tasks = load() を呼ぶ
> 2. リストを変更する
> 3. save(tasks) を呼ぶ
> - global next_id と変数 next_id は削除してよい。ID は len(tasks) + 1 で採番できる。
ドリル 3: JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ
ドリル名: 03-add-query-filter
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
base/main.py は JSONファイルCRUDが動く状態。GET /tasks にクエリパラメータ done を追加せよ。
仕様
| リクエスト | レスポンス |
|---|---|
GET /tasks | 全件返す(変更なし) |
GET /tasks?done=true | done: true のタスクだけ返す |
GET /tasks?done=false | done: false のタスクだけ返す |
GET /tasks?done=invalid | 400 Bad Request |
FastAPI でのクエリパラメータの受け取り方
パスパラメータと違い、クエリパラメータは関数の引数にデフォルト値付きで書く。
@app.get("/tasks")
def get_tasks(done: str | None = None):
...done=Noneのとき → クエリパラメータなし(全件返す)done="true"のとき → 完了タスクのみdone="false"のとき → 未完了タスクのみdoneがそれ以外の文字列 → 400
確認方法
- タスクをいくつか追加して done が混在した状態にする
GET /tasks→ 全件返ることGET /tasks?done=true→ 完了タスクのみ返ることGET /tasks?done=false→ 未完了タスクのみ返ることGET /tasks?done=yes→ 400 が返ること
> Swagger UI では GET /tasks に done パラメータが自動で表示される。
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
doneをbool | None型 (FastAPI が自動でtrue/falseを bool に変換) で受け取り直し、文字列ベースの実装との違いを比較するcreated_after/created_beforeのような日付クエリを追加して期間フィルタを実装する (created_atフィールドの追加が必要)sortクエリ (asc/desc) でid降順・昇順を切り替えられるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 3: ヒント
ドリル名: 03-add-query-filter
> ⚠️ 自力で考えてから読んでください
done: str | None = Noneで「省略可能なクエリパラメータ」を宣言できる- クエリパラメータの値は文字列として届く(
"true"/"false")。boolではないことに注意 - 許容する値:
None(省略)/"true"/"false"の3種類。それ以外はraise HTTPException(400, ...) - 絞り込みには
[t for t in tasks if t["done"] == ...]のリスト内包表記を使う "true"→True、"false"→Falseへの変換はdone_value = done == "true"で1行でできる
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ch02-json 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の構造は そのまま
- ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しいエンドポイント/機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する I/F (URL・HTTPメソッド・レスポンスのキー名) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
main.pyから 自力で全部書く - ファイル構造・関数分割・命名・内部ロジック: 完全に自由
- 守るのはインターフェースだけ: URL・HTTP メソッド・レスポンス JSON 形式
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
main.py) は変えない tests/フォルダは触らない (採点ロジック)
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-load-save | json.load / json.dump - ファイル読み書き | 🔧 穴埋め | ★ |
| 2 | 02-add-save | json.dump - JSON ファイルへの保存 | ➕ 機能追加 | ★ |
| 3 | 03-add-query-filter | JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ | ➕ 機能追加 | ★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: json.load / json.dump - ファイル読み書き
ドリル名: 01-fill-load-save
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を3箇所埋めて、JSONファイルへの読み書きを完成させよ。
def load() -> list[dict]:
if not DB_FILE.___(): # ファイルの存在確認メソッドを埋める
return []
return json.___(DB_FILE.read_text(encoding="utf-8")) # JSON文字列→Pythonオブジェクト変換を埋める
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.___(tasks, ensure_ascii=False, indent=2), # Pythonオブジェクト→JSON文字列変換を埋める
encoding="utf-8"
)確認方法
POST /tasks→{"title": "牛乳を買う"}を送信する- カレントディレクトリに
tasks.jsonが作成されること GET /tasksでタスク一覧が返ること
ドリル 1: ヒント
ドリル名: 01-fill-load-save
> ⚠️ 自力で考えてから読んでください
> - pathlib.Path にはファイルが存在するかどうかを確認するメソッドがある。辞書の in チェックのような使い方で、真偽値を返す。
> - json モジュールには文字列とPythonオブジェクトを相互変換する関数が2つある。
> - 文字列 → Python オブジェクト: json.loads(s は string の s)
> - Python オブジェクト → 文字列: json.dumps(s は同じく string の s)
ドリル 2: json.dump - JSON ファイルへの保存
ドリル名: 02-add-save
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
問題
base/main.py のメモリ実装を、JSONファイル永続化に書き直せ。
仕様:
jsonとpathlib.PathをインポートするDB_FILE = Path("tasks.json")を定義するload()/save()関数を追加する- 各エンドポイントでメモリ変数
tasksの代わりにload()を使う - 変更があるエンドポイントは変更後に
save()を呼ぶ global next_idは不要になる。ID採番はlen(tasks) + 1で行う
確認方法
POST /tasks→{"title": "牛乳を買う"}を送信する- サーバーを
Ctrl+Cで停止し、再起動する GET /tasksでタスクが残っていること
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
save()内でtasks.json.bakというバックアップファイルも一緒に書き出して、tasks.jsonが壊れたとき復旧できるようにする- ID 採番を
len(tasks) + 1から「現在保持している最大 ID + 1」に変更し、削除後に追加しても ID が衝突しないようにする load()に簡易ログ (print(f"loaded {len(tasks)} tasks")) を入れて、起動時にどれだけ読み込んだか確認する
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-save
> ⚠️ 自力で考えてから読んでください
> - ファイルの先頭に import json と from pathlib import Path を追加する。
> - DB_FILE = Path("tasks.json") をアプリ直下(app = FastAPI() の近く)に定義する。
> - load() / save() の実装は fill-load-save で埋めたものと同じ。
> - 各エンドポイントの書き換えパターンは共通:
> 1. 関数の先頭で tasks = load() を呼ぶ
> 2. リストを変更する
> 3. save(tasks) を呼ぶ
> - global next_id と変数 next_id は削除してよい。ID は len(tasks) + 1 で採番できる。
ドリル 3: JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ
ドリル名: 03-add-query-filter
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
base/main.py は JSONファイルCRUDが動く状態。GET /tasks にクエリパラメータ done を追加せよ。
仕様
| リクエスト | レスポンス |
|---|---|
GET /tasks | 全件返す(変更なし) |
GET /tasks?done=true | done: true のタスクだけ返す |
GET /tasks?done=false | done: false のタスクだけ返す |
GET /tasks?done=invalid | 400 Bad Request |
FastAPI でのクエリパラメータの受け取り方
パスパラメータと違い、クエリパラメータは関数の引数にデフォルト値付きで書く。
@app.get("/tasks")
def get_tasks(done: str | None = None):
...done=Noneのとき → クエリパラメータなし(全件返す)done="true"のとき → 完了タスクのみdone="false"のとき → 未完了タスクのみdoneがそれ以外の文字列 → 400
確認方法
- タスクをいくつか追加して done が混在した状態にする
GET /tasks→ 全件返ることGET /tasks?done=true→ 完了タスクのみ返ることGET /tasks?done=false→ 未完了タスクのみ返ることGET /tasks?done=yes→ 400 が返ること
> Swagger UI では GET /tasks に done パラメータが自動で表示される。
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
doneをbool | None型 (FastAPI が自動でtrue/falseを bool に変換) で受け取り直し、文字列ベースの実装との違いを比較するcreated_after/created_beforeのような日付クエリを追加して期間フィルタを実装する (created_atフィールドの追加が必要)sortクエリ (asc/desc) でid降順・昇順を切り替えられるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 3: ヒント
ドリル名: 03-add-query-filter
> ⚠️ 自力で考えてから読んでください
done: str | None = Noneで「省略可能なクエリパラメータ」を宣言できる- クエリパラメータの値は文字列として届く(
"true"/"false")。boolではないことに注意 - 許容する値:
None(省略)/"true"/"false"の3種類。それ以外はraise HTTPException(400, ...) - 絞り込みには
[t for t in tasks if t["done"] == ...]のリスト内包表記を使う "true"→True、"false"→Falseへの変換はdone_value = done == "true"で1行でできる
ch02-json 演習解説
⚠️ 自力でドリルを解いてから読んでください
ドリル 1: json.load / json.dump - ファイル読み書き — 模範解答
ドリル名: 01-fill-load-save
import json
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
DB_FILE = Path("tasks.json")
class Task(BaseModel):
title: str
done: bool = False
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.dumps(tasks, ensure_ascii=False, indent=2),
encoding="utf-8"
)
@app.get("/tasks")
def get_tasks():
return load()
@app.post("/tasks", status_code=201)
def create_task(task: Task):
tasks = load()
new_task = {"id": len(tasks) + 1, **task.model_dump()}
tasks.append(new_task)
save(tasks)
return new_taskドリル 1: json.load / json.dump - ファイル読み書き — 解説
ドリル名: 01-fill-load-save
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.dumps(tasks, ensure_ascii=False, indent=2),
encoding="utf-8"
)> ポイント: loads / dumps の末尾の s は string の s。
> - loads — load from string(文字列を受け取ってPythonオブジェクトを返す)
> - dumps — dump to string(PythonオブジェクトをJSON文字列に変換する)
>
> ファイルから読むときは read_text() で文字列にしてから loads、
> ファイルに書くときは dumps で文字列にしてから write_text() に渡す、
> という2段構えを覚えておくと混乱しない。
ドリル 2: json.dump - JSON ファイルへの保存 — 模範解答
ドリル名: 02-add-save
import json
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
DB_FILE = Path("tasks.json")
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
@app.get("/tasks")
def get_tasks():
return load()
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
for task in load():
if task["id"] == task_id:
return task
raise HTTPException(status_code=404, detail="Task not found")
@app.post("/tasks", status_code=201)
async def create_task(request: Request):
body = await request.json()
tasks = load()
new_task = {"id": len(tasks) + 1, "title": body["title"], "done": body.get("done", False)}
tasks.append(new_task)
save(tasks)
return new_task
@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, request: Request):
body = await request.json()
tasks = load()
for t in tasks:
if t["id"] == task_id:
t.update({"title": body["title"], "done": body["done"]})
save(tasks)
return t
raise HTTPException(status_code=404, detail="Task not found")
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
tasks = [t for t in load() if t["id"] != task_id]
save(tasks)ドリル 2: json.dump - JSON ファイルへの保存 — 解説
ドリル名: 02-add-save
import json
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
DB_FILE = Path("tasks.json")
class Task(BaseModel):
title: str
done: bool = False
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(
json.dumps(tasks, ensure_ascii=False, indent=2),
encoding="utf-8"
)
@app.get("/tasks")
def get_tasks():
return load()
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
tasks = load()
for task in tasks:
if task["id"] == task_id:
return task
raise HTTPException(status_code=404, detail="Task not found")
@app.post("/tasks", status_code=201)
def create_task(task: Task):
tasks = load()
new_task = {"id": len(tasks) + 1, **task.model_dump()}
tasks.append(new_task)
save(tasks)
return new_task
@app.patch("/tasks/{task_id}")
def update_task(task_id: int, task: Task):
tasks = load()
for t in tasks:
if t["id"] == task_id:
t.update(task.model_dump())
save(tasks)
return t
raise HTTPException(status_code=404, detail="Task not found")
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
tasks = load()
tasks = [t for t in tasks if t["id"] != task_id]
save(tasks)> ポイント: 変更のたびに load → 変更 → save の3ステップを踏む。
> リクエストをまたいでメモリ上の tasks を使い回さないことが永続化の核心。
>
> 補足: global next_id が不要になった理由は、ID を len(tasks) + 1 で採番するようにしたため。
> ただしタスクを削除すると ID が重複する可能性があるため、本格的なアプリでは UUID や
> データベースのオートインクリメントを使うのが一般的。
ドリル 3: JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ — 模範解答
ドリル名: 03-add-query-filter
import json
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
DB_FILE = Path("tasks.json")
def load() -> list[dict]:
if not DB_FILE.exists():
return []
return json.loads(DB_FILE.read_text(encoding="utf-8"))
def save(tasks: list[dict]) -> None:
DB_FILE.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
@app.get("/tasks")
def get_tasks(done: str | None = None):
tasks = load()
if done is None:
return tasks
if done not in ("true", "false"):
raise HTTPException(status_code=400, detail="done must be 'true' or 'false'")
done_value = done == "true"
return [t for t in tasks if t["done"] == done_value]
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
for task in load():
if task["id"] == task_id:
return task
raise HTTPException(status_code=404, detail="Task not found")
@app.post("/tasks", status_code=201)
async def create_task(request: Request):
body = await request.json()
tasks = load()
new_task = {"id": len(tasks) + 1, "title": body["title"], "done": body.get("done", False)}
tasks.append(new_task)
save(tasks)
return new_task
@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, request: Request):
body = await request.json()
tasks = load()
for t in tasks:
if t["id"] == task_id:
t.update({"title": body["title"], "done": body["done"]})
save(tasks)
return t
raise HTTPException(status_code=404, detail="Task not found")
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
tasks = [t for t in load() if t["id"] != task_id]
save(tasks)ドリル 3: JSON 読み込み + クエリパラメータ - ファイル経由のフィルタ — 解説
ドリル名: 03-add-query-filter
@app.get("/tasks")
def get_tasks(done: str | None = None):
tasks = load()
if done is None:
return tasks
if done not in ("true", "false"):
raise HTTPException(status_code=400, detail="done must be 'true' or 'false'")
done_value = done == "true"
return [t for t in tasks if t["done"] == done_value]ポイント
クエリパラメータの宣言 — def get_tasks(done: str | None = None): と書くだけで FastAPI がURLの ?done=... を受け取る。パスパラメータと違い {} は不要。
done は文字列で届く — URLのクエリ文字列はすべて str として渡される。bool への暗黙変換はない。done == "true" で bool に変換する。
バリデーション位置 — if done not in ("true", "false"): を絞り込み前に置く。None チェックを先に済ませておかないと None not in (...) が True になってしまう。
別解
# bool 型のクエリパラメータを FastAPI に任せる方法
@app.get("/tasks")
def get_tasks(done: bool | None = None):
tasks = load()
if done is None:
return tasks
return [t for t in tasks if t["done"] == done]done: bool | None = None と書くと FastAPI が "true"/"false" → True/False に自動変換し、それ以外は 422 を返す。"invalid" のとき 400 ではなく 422 になるが動作は同じ。どちらの実装を選ぶかは要件次第。