APIの部分更新(PATCH)を正しく設計する:データモデリングのアプローチ
はじめに:なぜ部分更新(PATCH)のデータモデリングを考える必要があるのか
RESTful APIを設計する際、リソースの作成(POST)や全体更新(PUT)、削除(DELETE)、取得(GET)は比較的明確な操作として定義できます。しかし、リソースの一部だけを変更したいという要望は多くの場合に発生します。例えば、ユーザーのメールアドレスだけを変更したい、商品の在庫数だけを調整したいといったケースです。
このような部分的な更新には、通常HTTPメソッドのPATCH
が用いられます。PUT
がリソース全体を置き換える操作であるのに対し、PATCH
はリソースに「差分を適用する」操作として定義されています。
PATCH
メソッドは非常に便利ですが、PUT
のようにリクエストボディの形式が明確に標準化されているわけではありません。どのような形式で「差分」を表現し、サーバー側でどのようにその差分を解釈してデータモデルに反映させるかは、API設計者が考慮すべき重要な点です。
特にデータモデリングの観点からは、以下の課題が生じがちです。
- どのフィールドを部分更新可能とするか? 読み取り専用のフィールドや、ビジネスロジック上単体での変更が許されないフィールドはどう扱うか。
- リクエストボディでどのような「差分」を表現するか? フィールドの値を変更するだけでなく、追加、削除、配列内の要素の操作などをどのように表現するか。
null
という値の意味合いをどう解釈するか? フィールドの値をnull
にするのか、それともそのフィールド自体を削除するのか。- ネストしたオブジェクトや配列の部分更新をどう表現するか?
これらの課題を解決し、効率的で保守性の高いAPIを設計するためには、PATCH
メソッドのためのデータモデリングのアプローチを体系的に理解することが不可欠です。この記事では、PATCH
メソッドを使った部分更新をデータモデリングの視点からどのように設計すべきか、代表的なリクエストボディの形式や考慮事項、実践的なポイントを解説します。
PATCHメソッドの基本的な考え方とPUTとの違い
改めて、PUT
とPATCH
の基本的な違いを確認しておきましょう。
- PUT: リクエストボディに含まれるデータで、指定されたリソースを完全に置き換えます。リクエストボディに含めなかったフィールドは、サーバー側でデフォルト値が設定されるか、あるいは削除される可能性もあります(サーバーの実装によります)。
PUT
は冪等性が保証されます。つまり、同じPUT
リクエストを何度実行しても、サーバー上のリソースの状態は初回実行後と同じになります。 - PATCH: リクエストボディで指定された「差分」を、指定されたリソースに適用します。リクエストボディのデータは、リソース全体を表すものではなく、変更したい部分とその新しい値を表現します。
PATCH
は冪等性が保証されない場合があります。例えば、「数値を1増やす」というPATCH
リクエストは、実行するたびに結果が変わります。ただし、リソースの状態を特定の値に設定するようなPATCH
リクエストであれば冪等になり得ます。
重要なのは、PATCH
が「差分を適用する」操作であるという点です。この「差分」をどのように表現するかが、データモデリングと密接に関わってきます。
データモデリング上の重要な考慮事項
PATCH
リクエストを受け付けるAPIエンドポイントを設計する際、データモデルの観点から以下の点を考慮する必要があります。
更新可能なフィールドの定義
全てのリソースフィールドがクライアントから自由に変更されて良いわけではありません。例えば、以下のようなフィールドはPATCH
の対象外とすべき場合が多いです。
- 読み取り専用フィールド: リソースのID、作成日時、最終更新日時(サーバー側で自動更新されるべき)、関連リソースへの不変の参照など。
- ビジネスロジック上、他のフィールドとセットで変更されるべきフィールド: 例外的なケースを除き、単体での変更がシステムの状態に不整合をきたすフィールド。
- 機密情報: 一部のセキュリティ上重要なフィールド(例: パスワードをPATCHで変更させるべきではない、専用のAPIを用意すべき)。
APIの仕様として、どのフィールドがPATCH
可能であるかを明確に定義し、ドキュメント化することが重要です。サーバー側では、PATCH
リクエストボディに含まれるフィールドが更新可能リストに含まれているかチェックし、不正なフィールドが含まれていればエラーレスポンス(例: 400 Bad Request)を返すように実装します。
ネストした構造と配列の扱い
リソースのデータモデルがネストしたオブジェクトや配列を含む場合、部分更新はより複雑になります。
例えば、ユーザーリソースが以下のような構造を持つとします。
{
"id": "user-123",
"name": {
"first": "Taro",
"last": "Yamada"
},
"email": "taro.yamada@example.com",
"address": {
"city": "Tokyo",
"street": "1-1 Chiyoda",
"zip": "100-0001"
},
"tags": ["developer", "japan"],
"createdAt": "2023-01-01T10:00:00Z"
}
- ネストしたオブジェクトの部分更新:
address.city
だけを変更したい場合、リクエストボディでどのように表現するか?単に{"address": {"city": "Osaka"}}
とすると、他のaddress
フィールド(street
,zip
)がどうなるか曖昧になります。全体置換になるのか、マージされるのか、形式に依存します。 - 配列の要素の操作:
tags
配列に新しいタグを追加したい、特定のタグを削除したい、既存のタグの値を変更したい、といった操作をどのように表現するか?配列全体を新しい配列で置き換えるのはPUT
でも可能ですが、PATCH
で部分的な操作(例: 2番目の要素を削除)を表現できるかがポイントです。
null
値の意味
多くのデータ形式(JSONなど)ではnull
という値を表現できます。しかし、PATCH
リクエストにおいて、リクエストボディの特定のフィールドの値がnull
である場合、その意味合いは曖昧になり得ます。
- そのフィールドの値を
null
に設定したいのか? - 既存のリソースからそのフィールドと値を削除したいのか?
この解釈も、採用するPATCH
リクエストボディの形式によって異なります。API設計者は、null
が何を意味するかを明確に定義し、ドキュメントで指定する必要があります。
代表的なPATCHリクエストボディの形式
PATCH
リクエストボディの形式に標準はありませんが、広く使われている代表的な形式がいくつか存在します。これらは「差分」の表現方法においてデータモデリングに影響を与えます。
1. JSON Merge Patch (RFC 7386)
JSON Merge Patchは、HTTP PATCH
メソッドでJSONリソースを更新するためのシンプルな形式です。リクエストボディはJSONオブジェクトであり、既存のリソースのJSONオブジェクトにマージ(結合)する形で差分を表現します。
- マージのルール:
- リクエストボディのJSONオブジェクトを、既存のリソースのJSONオブジェクトに再帰的にマージします。
- リクエストボディに存在するキーは、既存のリソースの同じキーの値を置き換えます。
- リクエストボディにのみ存在するキーは、既存のリソースに追加されます。
- リクエストボディに存在するキーの値が
null
の場合、既存のリソースからそのキーと値が削除されます。 - 配列は、リクエストボディの値で既存のリソースの配列全体を置き換えます。配列の要素単位での追加や削除は表現できません。
例: 元のリソース:
{
"name": "Taro",
"email": "taro.yamada@example.com",
"address": {
"city": "Tokyo",
"zip": "100-0001"
},
"tags": ["developer"]
}
PATCHリクエストボディ (JSON Merge Patch):
{
"email": "yamada.taro@example.com",
"address": {
"city": "Osaka"
},
"tags": ["developer", "jp"],
"age": 30
}
適用後のリソース:
{
"name": "Taro",
"email": "yamada.taro@example.com",
"address": {
"city": "Osaka",
"zip": "100-0001"
},
"tags": ["developer", "jp"],
"age": 30
}
(name
は変更なし、address
内のzip
はマージによって保持、tags
は全体が置き換え、age
が追加されています)
例2 (フィールド削除): 元のリソース(上の適用後リソースと同じ)
PATCHリクエストボディ (JSON Merge Patch):
{
"address": null,
"age": null
}
適用後のリソース:
{
"name": "Taro",
"email": "yamada.taro@example.com",
"tags": ["developer", "jp"]
}
(address
とage
フィールドが削除されました)
メリット: * 形式がシンプルで直感的。PUTリクエストのボディ構造と似ているため理解しやすい。 * 実装が比較的容易。
デメリット:
* 配列の部分的な操作(要素の追加/削除/変更)が表現できない。
* フィールドを明示的にnull
値に設定したい場合、JSON Merge Patchではそれがフィールド削除と解釈されるため表現できない。
2. JSON Patch (RFC 6902)
JSON Patchは、リソースに適用する一連の操作を記述するための形式です。リクエストボディは、適用すべき操作オブジェクトの配列になります。各操作オブジェクトは、操作の種類(op
)、操作の対象を示すポインター(path
、JSON Pointer形式)、および必要に応じて値(value
)などを含みます。
-
操作の種類 (
op
):add
: 指定されたパスに値を追加(オブジェクトへのキー追加、配列への要素追加)。remove
: 指定されたパスの値を削除(オブジェクトからキー削除、配列から要素削除)。replace
: 指定されたパスの値を置き換え。copy
: 指定されたパスに、他のパスの値をコピー。move
: 指定されたパスの値を、他のパスに移動。test
: 指定されたパスの値が、指定された値と一致するかをテスト(アトミックな操作のための前提条件チェックに使う)。
-
パス (
path
): JSON Pointer (RFC 6901) 形式で、リソースJSON構造内の特定の場所を指定します。/name/last
はname
オブジェクトの中のlast
キーを指します。/tags/1
はtags
配列のインデックス1の要素を指します。
例: 元のリソース:
{
"name": {
"first": "Taro",
"last": "Yamada"
},
"email": "taro.yamada@example.com",
"address": {
"city": "Tokyo",
"street": "1-1 Chiyoda"
},
"tags": ["developer", "japan"]
}
PATCHリクエストボディ (JSON Patch):
[
{ "op": "replace", "path": "/email", "value": "yamada.taro@example.com" },
{ "op": "replace", "path": "/address/city", "value": "Osaka" },
{ "op": "add", "path": "/tags/-", "value": "programmer" },
{ "op": "remove", "path": "/tags/1" }
]
(/tags/-
は配列の末尾に要素を追加するというJSON Pointerの特殊な記法です。/tags/1
は元の"japan"タグを指します。)
適用後のリソース:
{
"name": {
"first": "Taro",
"last": "Yamada"
},
"email": "yamada.taro@example.com",
"address": {
"city": "Osaka",
"street": "1-1 Chiyoda"
},
"tags": ["developer", "programmer"]
}
(メールアドレスと都市が変更され、"programmer"タグが追加され、元の"japan"タグが削除されています)
メリット:
* JSON構造に対するあらゆる種類の変更(追加、削除、置換、配列操作など)を非常に表現力豊かに記述できる。
* RFCとして標準化されている。
* null
値を値として扱いたい場合も、"value": null
と指定することで表現可能(フィールド削除はremove
操作で行う)。
デメリット: * JSON Merge Patchに比べて形式が複雑で、リクエストボディの作成・解釈にJSON Pointerなどの知識が必要となる。 * 多数のフィールドを変更する場合、リクエストボディが冗長になることがある。
3. カスタム形式
上記の標準形式を使用せず、APIの設計者が独自に定義した形式で差分を表現する方法です。
例: ユーザーリソースの一部更新を、変更したいフィールド名と値のリストで表現する。
{
"updates": [
{ "field": "email", "value": "yamada.taro@example.com" },
{ "field": "address.city", "value": "Osaka" },
{ "field": "tags", "action": "add", "value": "programmer" },
{ "field": "tags", "action": "remove", "value": "japan" }
]
}
または、フィールド名をキーとするシンプルな形式で、ネストや配列操作は特別な記法や形式で表現するなど。
メリット: * 特定のドメインやユースケースに特化することで、最もシンプルで分かりやすい形式を設計できる可能性がある。 * 必要な操作だけを表現できる。
デメリット: * 標準形式ではないため、クライアントとサーバー間の仕様が密結合になり、クライアント実装者に学習コストがかかる。 * 汎用的なライブラリが存在しない場合が多く、実装コストがかかる。 * 形式の設計自体に考慮が必要で、後から拡張しにくい設計になるリスクがある。
どの形式を選択すべきか?
どのPATCH
リクエストボディ形式を選択するかは、APIの要件、必要な表現力、シンプルさ、クライアント側の実装容易性などを総合的に考慮して判断します。
- シンプルさ、主にトップレベルまたはネストレベル1のフィールドの値を変更したい場合: JSON Merge Patchが適しています。配列の要素単位の操作や、
null
値による削除が不要であれば、最も導入しやすい選択肢です。 - 配列の要素単位の操作や、より複雑なJSON構造への変更(追加/削除/移動など)が必要な場合: JSON Patchが強力な選択肢となります。表現力が高く、多くのケースに対応できます。ただし、その複雑さをクライアント・サーバー双方が許容できるか検討が必要です。
- 特定のドメインに特化し、標準形式では不自然になるような更新操作が多い場合: カスタム形式も選択肢に入ります。ただし、標準形式のデメリット(学習コスト、実装コスト、密結合)を理解した上で、それを上回るメリットがあるかを慎重に評価する必要があります。可能な限り標準形式(JSON Merge PatchやJSON Patch)に準拠することを推奨します。
設計上のアンチパターン
PATCH
メソッドを使ったデータモデリングで避けるべき代表的なアンチパターンを挙げます。
- 全てのフィールドをPATCH可能にしてしまう: 読み取り専用フィールドや、セキュリティ上・ビジネスロジック上変更すべきでないフィールドまで
PATCH
で更新できてしまう設計は危険です。更新可能なフィールドは明確に定義し、サーバー側で厳格にチェックする必要があります。 - PATCHリクエストボディの形式や解釈が曖昧である: クライアントがどのような形式でリクエストボディを作成すれば良いか、また
null
値やネストした構造がどのように解釈されるかが不明確な場合、APIの利用者は混乱し、誤った実装を招きます。形式は明確に定義し、OpenAPI Specなどで詳細にドキュメント化すべきです。 - エラーハンドリングが不十分:
PATCH
リクエストが失敗した場合に、どのフィールドに対するどの操作がなぜ失敗したのかがクライアントに伝わらない設計は不親切です。具体的なエラー情報を含むレスポンスを返すように設計します(例: RFC 7807 Problem Details形式など)。 - 部分更新操作の検証を怠る: 受け取ったリクエストボディが形式的に正しいか、更新内容がデータモデルやビジネスルールに適合しているか(例: 必須フィールドをnullにしようとしていないか、不正な値を設定しようとしていないかなど)をサーバー側で十分に検証する必要があります。
実践的な考慮事項
PATCH
APIを設計し実装するにあたって、データモデリング以外にも考慮すべき実践的なポイントがあります。
- ドキュメント化: 採用した
PATCH
リクエストボディの形式、更新可能なフィールドとその制約、null
値の解釈、エラーレスポンスの詳細などをAPIドキュメントで明確に記述します。 - バリデーション: リクエストボディの形式妥当性、JSON Pointerの有効性(JSON Patchの場合)、フィールドの型や値の妥当性、ビジネスロジック上の整合性など、様々なレベルでバリデーションを実装します。
- 冪等性: 可能な限り、
PATCH
リクエストが冪等になるように設計することを検討します。例えば、「ステータスを完了にする」という操作は冪等になり得ますが、「在庫数を1減らす」は冪等ではありません。冪等でない操作の場合、クライアントはリトライ戦略に注意が必要です。 - 競合回避: 複数のクライアントが同時に同じリソースを
PATCH
で更新しようとした場合に、予期しない結果にならないように、楽観的ロック(Etagなどを使用)や悲観的ロックといった競合回避策を検討します。 - バージョンニング: APIのバージョンアップに伴い、
PATCH
で更新可能なフィールドやリクエストボディの形式を変更する必要が生じる可能性があります。APIバージョンニング戦略の一部として、PATCH
エンドポイントの変更も考慮に入れます。
まとめ
PATCH
メソッドは、RESTful APIでリソースの部分更新を行うための強力な手段です。しかし、その柔軟性の高さゆえに、どのような「差分」をどのような形式で表現し、サーバー側でどのようにデータモデルに適用するかについて、設計者は明確な方針を持つ必要があります。
この記事では、PATCH
メソッドのためのデータモデリングにおいて、更新可能なフィールドの定義、ネスト構造や配列の扱い、null
値の解釈といった重要な考慮事項があることを解説しました。また、代表的なリクエストボディ形式としてJSON Merge PatchとJSON Patchを紹介し、それぞれの特徴や適したケース、そしてカスタム形式のメリット・デメリットについて触れました。
正しいPATCH
APIを設計するためには、単に技術的な形式を選ぶだけでなく、ビジネス要件に基づいて更新可能なフィールドを適切に定義し、選択した形式のルールを明確にドキュメント化し、堅牢なバリデーションとエラーハンドリングを実装することが重要です。これらのデータモデリングと設計のアプローチを理解し実践することで、より効率的で保守性の高いRESTful APIを構築することができるでしょう。