API を作っていると、ユーザー情報を返す場面がやたら多いことに気づきます。友達一覧、メンバー一覧、メッセージの送信者、検索結果……。
これを場面ごとに考えていると、同じユーザーなのに返ってくる形がバラバラになります。そこで「一覧に埋め込むためのユーザー情報」を専用のモデルとして1つ用意するやり方があります。私は UserSummary という名前を付けていて、この記事では「Summary 型」と呼びます。
FastAPI + Pydantic を例にしますが、考え方はどの言語でも同じです。
ちなみに FastAPI には
@router.get(summary="...")という別の機能もあります(/docsの見出しになるやつ)。この記事はそれとは関係なく、レスポンスに使うモデルの設計の話です。
ユーザー情報を返す場面は思ったより多い
まず、実際にどれくらいあるかを見てみます。私が作っているチャットアプリだと、これだけありました。
- 友達一覧
- ユーザー検索の結果
- 友達申請の一覧(送信・受信)
- QRコードを読み取った結果
- ルームのメンバー一覧
- メッセージの送信者
- 添付ファイルの投稿者
7箇所です。しかも下2つのように、「ユーザーの一覧」ではないのにユーザー情報がくっついてくるパターンもあります。
ここで何も考えずに作ると、こうなりがちです。
// 友達一覧のレスポンス
{ "id": "...", "username": "山田", "icon_url": "...", "created_at": "..." }
// メッセージの送信者
{ "user_id": "...", "name": "山田", "icon": "..." }
// メンバー一覧
{ "id": "...", "username": "山田", "icon_url": "...", "role": "admin" }
同じ人なのに、キーの名前も入っている項目も違います。 こうなると画面を作る側は、場面ごとに別の処理を書くことになります。
最小情報を Summary 型にまとめる
そこで、ユーザーを1行で表示するのに必要な情報だけを集めたモデルを1つ作ります。
class UserSummary(BaseModel):
id: uuid.UUID # ユーザーID
username: str # 名前
icon_url: str | None # アイコン画像
online_status: str # オンラインかどうか
たったこれだけです。そして、ユーザー情報を返す場面では全部このモデルを使うと決めます。
class FriendResponse(BaseModel):
id: int
user: UserSummary # ← 埋め込む
class MessageResponse(BaseModel):
content: str
sender: UserSummary # ← ここも同じモデル
図にすると、1枚の共通プロフィールカードを、必要な画面で使い回すイメージです。
呼び出し元では user や sender と名前が変わっても、その中に入るユーザー情報は UserSummary にそろいます。
返ってくる JSON はこうなります。
// 友達一覧
{ "id": 12, "user": { "id": "...", "username": "山田", "icon_url": "...", "online_status": "online" } }
// メッセージ
{ "content": "こんにちは", "sender": { "id": "...", "username": "山田", "icon_url": "...", "online_status": "online" } }
user と sender で名前は違いますが、中身の形はまったく同じです。
こうしておくと、画面側は「ユーザーカード」を表示する部品を1つ作れば、どこでも使い回せます。名刺のフォーマットを統一するようなイメージですね。
自分用と他人用は、モデルごと分ける
もう1つ大事なのが、自分のプロフィールを返すモデルは別に用意することです。
class UserResponse(BaseModel): # 自分用(GET /users/me)
id: uuid.UUID
username: str
email: str # ← 他人には見せない
icon_url: str | None
status_message: str | None
online_status: str
qr_token: str # ← 他人には見せない
organization_id: uuid.UUID | None
created_at: datetime
updated_at: datetime
自分用のほうには、メールアドレスやQRトークンといった本人にしか見せてはいけない情報が入っています。
「1つのモデルにまとめて、他人に返すときだけ項目を消す」というやり方もありますが、おすすめしません。消し忘れたら露出するからです。
モデルごと分けておけば、友達一覧のコードで UserSummary を使っている限り、メールアドレスは構造的に出しようがありません。ここは頑張って気をつけるところではなく、仕組みで防ぐところだと思っています。
何を入れて、何を入れないか
判断に迷ったら、この基準で考えています。
その一覧の1行を描くのに、その項目は要るか?
これで並べると、こうなります。
| 項目 | 入れる? | 理由 |
|---|---|---|
| 名前 | 入れる | 名前がないと誰だかわからない |
| アイコン | 入れる | 一覧で顔が見えるかは体験に直結する |
| オンライン状態 | 入れる | 一覧に緑の点を出したい |
| ユーザーID | 入れる | タップしたときの遷移先に要る |
| 登録日時 | 入れない | 一覧に出す場面がない |
| 組織ID | 入れない | 表示には使わない |
| メールアドレス | 入れない | そもそも他人に見せない |
迷ったら「入れない」
もう1つ、迷ったら入れないをおすすめします。
理由は単純で、後から足すのは簡単だけど、消すのは大変だからです。
一度返してしまった項目は、どこかの画面がもう使っているかもしれません。消すには全画面を確認する必要があります。逆に、足すのは既存の画面に影響しません(知らない項目は無視されるだけです)。
なので「使うかもしれないから入れておこう」ではなく、**「使うことがわかってから足す」**のほうが安全です。
実装: 丸ごと渡せるようにしておく
FastAPI(Pydantic v2)なら、こう書けます。
class UserSummary(BaseModel):
model_config = ConfigDict(from_attributes=True) # ← これが効く
id: uuid.UUID
username: str
icon_url: str | None = None
online_status: str
from_attributes=True を付けておくと、データベースから取ってきたユーザーを丸ごと渡せるようになります。
user = db.get(User, user_id) # データベースから丸ごと取って
UserSummary.model_validate(user) # 丸ごと渡す
この書き方だと、UserSummary に書いてある項目だけが自動で拾われ、それ以外(メールアドレスなど)は渡しても勝手に捨てられます。
一方、こう書いてしまうと後がしんどくなります。
# 手で1つずつ書く書き方
UserSummary(
id=user.id,
username=user.username,
icon_url=user.icon_url,
online_status=user.online_status,
)
動きは同じですが、項目を1つ足したくなったとき、この書き方をしている全箇所を直す必要があります。7箇所あれば7箇所です。
実際、私のところには後から「ステータスメッセージも一覧に出したい」という要望が来ました。丸ごと渡す書き方にしてあったので、モデルに1行足すだけで7箇所すべてに反映されました。この話は別記事に書きます。
まとめ
- ユーザー情報を返す場面は思ったより多い。場面ごとに考えると形がバラバラになる
- 「一覧の1行を描くための最小情報」をモデルにして、全部そこを通す
- 自分用と他人用はモデルごと分ける。消し忘れによる情報露出を仕組みで防げる
- 入れるか迷ったら入れない。足すのは簡単、消すのは大変
もし今、ユーザー情報を返す API を書いていて画面ごとにレスポンスの形が違っているなら、まず「どの画面でも共通して出している項目」を書き出してみるのが始めやすいです。だいたい名前とアイコンとIDあたりに落ち着くはずで、それがそのまま最初の Summary 型になります。
参考
-
レスポンスモデル - 戻り値の型 — FastAPI 公式ドキュメント …
response_modelで出力する項目を絞る、公式のやり方 - 追加のモデル — FastAPI 公式ドキュメント … 入力用・出力用・DB保存用でモデルを分ける例。「他人に見せない項目はモデルごと分ける」は公式もこの形
-
Models — Pydantic 公式ドキュメント …
from_attributesとmodel_validateで、DBから取ったオブジェクトを丸ごと渡す話 -
Configuration(ConfigDict) — Pydantic 公式ドキュメント …
ConfigDictに指定できる設定の一覧
関連記事
- 初めてのAI駆動開発。勤怠管理アプリを1ヶ月で本番公開した全8ステップ … この記事と同じ「開発エンジニアになってから初めてやったこと」シリーズ。設計より前の、アプリを1本出しきるまでの流れはこちら
- AWSのDB、コンソールで中身が見えるやつと見えないやつがありました … 同じアプリのDB側の話。Summary型が返す中身を、結局APIのレスポンスで確認することになった経緯が書いてあります
-
AWS RDSとは? DBを運用込みで借りるサービス …
db.get(User, user_id)の向こう側にあるDBそのものの入門。RDSとAuroraの関係から
