API設計において、私はGoogleのAIP(API Improvement Proposals)やStyle Guideを参考にすることが多いんですが。
その中のGoogle JSON Style Guideには、「NULL値のプロパティは、プロパティごと削除することを検討する」という規約があります。
つまり、 name と age を持つ Users リソースで age が未入力(NULL)であれば、JSONは以下が推奨です。
{
"name": "Kazuya Umeki"
}
ただ、このRuleを採用すると、PATCH(部分更新)の実装で課題が出てきます。
この課題に対して、これまで2つのアプローチを採用しました。本記事ではそのプラクティスを紹介します。
前提
Google Style Guide とは
主要なOSSプロジェクトには、独自のStyle Guideがあります。
皆さんのプロジェクトでも、粒度問わず慣習やルールといったものがあると思います。
例えば。
「変数名はキャメルケース」「クォートはダブルクォート」のような、コードの書き方のルール集。
Google Style Guild はプロジェクトでコードをどう書くかについて、Google がまとめたものです。
「なんでそう決めたか」の言及もあるため、眺めてみるだけでも結構学びになります。
最近だとドキュメントを書くことが増えたので、時折以下も見てるかも。
ドキュメント内にちょいちょいこういう記事が転がってるので結構面白いですよ。
余談: 私が Google の Style Guide を採用している理由
何社か他のStyleGuideも精査はしたんですが、以下に強みがあると思います。
- 根拠が明記されている: ルールだけでなく、理由やトレードオフが説明されていることが多い。
- 親しみやすい: 知名度が高く採用実績も多いので、導入コストが低く、コードレビューや新規プロジェクトの立ち上げが楽になる。そのまま採用しても、フォークしてカスタマイズしてもよい。
この辺りの採用理由については、気が向けば記事にしてみたいなと思います。
Google の Style Guide は個人の好みではなく可読性や保守性を重視している点が魅力だと感じています。
ただし、あくまでガイドラインです。
言語によってはより規範的な標準があり、たとえばGoなら Effective Go や gofmt を最優先にして、Googleのスタイルガイドは補足的な位置づけにしています。
'Empty/Null Property Values' とは
では本題の Property が NULL 値のケースについて。
Google JSON Style Guideには Empty/Null Property Values という規約があります。
Consider removing empty or null values.
If a property is optional or has an empty or null value, consider dropping the property from the JSON, unless there’s a strong semantic reason for its existence.
{ "volume": 10, // Even though the "balance" property's value is zero, it should be left in, // since "0" signifies "even balance" (the value could be "-1" for left // balance and "+1" for right balance. "balance": 0 // The "currentlyPlaying" property can be left out since it is null. // "currentlyPlaying": null }
※ 引用中のコメントは説明用で、実際のJSONにコメントは書けません。
つまり、「特に理由がない限り、空やNULLのプロパティはJSONから削除する」ということです。
上の例では、balance は -1 や 1 を取り得るため、0 に意味があります。だから省かずに残しているのだろう、と私は解釈しています。
この規約は、リクエストにもレスポンスにも適用されます。
強い理由(=意味を持つ値)がある場合に残す、という考え方です。
どういう方針でもよいのですが型が厳格な言語の採用が増えてますので、「こういった規約が存在していること」がありがたいですよね。
undefined なのか null なのか迷わなくて済みますよね。
ただ、これが今回の問題の要因です。
PATCH(HTTP Request)について
PATCH は HTTP のリクエストメソッドで、リソースへの部分的な変更を適用します。
PUTは Resource の完全な置き換えをします。つまり冪等。
PATCHは Resource に対して部分的な変更をします。つまり冪等じゃない。(設計次第ですが)
比較するとこんな感じです。
curl -X 'PATCH' \
-H 'Content-Type: application/json' \
"https://api.example.com/users/{id}" \
-d '{"name": "Kazuya Umeki"}'
curl -X 'PUT' \
-H 'Content-Type: application/json' \
"https://api.example.com/users/{id}" \
-d '{"name": "Kazuya Umeki", "age": 25}'
あれ、これ age を NULL にできなくね?
お気づきの方もいると思いますが。
そうです。PATCH では、age を後から未入力に戻すには課題があるんですよね。
どういうことか説明いたしますね。例えば、リソースが次の状態だとします。
{
"name": "Kazuya Umeki dayo",
"age": 25
}
ここで、「年齢公開したくないんだよなぁ。」という気持ちが過ぎります。
ageをNULLにしようと、以下のような Request を試みます。
curl -X 'PATCH' \
-H 'Content-Type: application/json' \
"https://api.example.com/users/{id}" \
-d '{"name": "Kazuya Umeki"}'
ただ、サーバー側のアプリケーションでは、「PATCH Request が来た。nameを更新する問い合わせなのね。」と解釈されるため結果は以下。
{
"name": "Kazuya Umeki",
"age": 25
}
一度入力した内容を、リセットできません。
整理すると、PATCHのボディには本来3つの状態があります。
| 状態 | 意味 | 規約に従ったJSON |
|---|---|---|
| 値あり | その値に更新したい | "age": 30 |
| NULL | 値を消したい | プロパティごと削除(=区別できない) |
| 未指定 | 更新対象外 | プロパティごと削除(=区別できない) |
NULLを省略する規約は、「消したい」と「更新対象外」の区別を失わせます。
これが今回の課題の要因です。とくに、型が厳格な言語(TypeScriptやGolangなど)では、この3状態を区別したい場面が多いはず。
年齢だと例えがピンとこない方もいるかなと思いますので、性別(gender)で置き換えても良いかもです。
次のセクションからは、「一度入力した 性別(gender) Property を Null にしたい」という要望に応える方法について、自身が検討 / 実装したアプローチ方法紹介します。
アプローチ
その前に検討した他の選択肢を紹介
私の2案の前に、他の標準的な方法にも触れておきます。
| 方法 | 「消したい」の表現 | 備考 |
|---|---|---|
| NULLの明示送信 | {"age": null} |
規約の例外条項を使う素朴な方法 |
| JSON Merge Patch(RFC 7396) |
{"age": null}=プロパティの削除 |
PATCHの標準的な表現 |
| JSON Patch(RFC 6902) | 操作の配列で remove を指定 |
表現力は高いが書式が別物 |
※ 採用しなかった理由は割愛させてください。ご容赦ください。
対応1: AIP-126 大作戦
AIP ご存知でしょうか?
※ 作戦名は同僚と考えました。Googleは関与していません。
AIP(API Improvement Proposals)は、GoogleがAPI設計の指針としてまとめている設計文書です。
その中の AIP-126 には Enum(列挙型)についての明示があります。
有用なゼロ値を扱う方法も書かれていて、今回はこれを採用しました。
具体的な方法は以下をご覧ください。NULL を値として列挙します。
type gender string
const (
genderUnspecified gender = "unspecified"
genderMale gender = "male"
genderFemale gender = "female"
)
当初は「ポインタ型にしようか」「string のままプロパティを省くか」など、どうやってオプショナルを扱うか考えていたんですが。
ただ、NULLという状態を値として扱うことにすれば、API通信・DB・UIの各レイヤで扱いが揃うと考えました。
一方で、この方法には限界(制約)があります。
- 列挙できるもの(Enum)にしか使えない
- 「未設定」と「回答しない」を同じ値にまとめてよいかは、要件次第
ということで、対応2を紹介します。
対応2: UpdateMask 大作戦
gRPC で使われる手法なんですが、PATCH通信でも応用が出来ないかなと思い、「PoC / 採用 / 実装」 しました。
gRPCで使われる手法なんですが、HTTP Request(PATCH)にも応用できないかと考えて、PoC・採用・実装しました。
根拠は AIP-134(Standard methods: Update)です。
簡単に言うと、更新対象のプロパティを明示する手法。
※ 作戦名は同僚と考えました。実質「AIP-134 大作戦」です。
見てもらったほうが早そうなので、とりあえず Request をご覧ください。
curl -X 'PATCH' \
-H 'Content-Type: application/json' \
"https://api.example.com/users/{id}?update_mask=name,gender" \
-d '{"name": "Taro Tanaka"}'
-
update_mask=name,genderのように、クエリパラメータで更新したいプロパティをカンマ区切りで明示する。 - Request Body は変更後の形(NULL 値は省く)を明示する。
サーバー(アプリケーション)の視点でみると以下な感じ。
- ユーザーリソースに対する更新依頼が来た!
- クエリパラメータを見ると、更新対象は
nameとgenderの2つだ! - ボディを見ると、
nameはTaro Tanakaに更新か、genderはボディにないので NULL(未設定)に更新だね。
このアプローチを採用すると、当初の課題であった 「消したい」と「更新対象外」を、規約に従ったまま区別できます。
先ほどの表に、この方式を足すとこうなります。
| リクエスト | サーバーの解釈 |
|---|---|
update_mask=name + ボディ {"name": "A"}
|
name だけ更新 |
update_mask=name,age + ボディ {"name": "A"}
|
name を更新し、age をNULLに |
| マスクなし | AIP-134では、値のあるフィールドすべてが対象 |
実装にあたって決める点もあります。
-
更新してはいけないフィールド(
idや権限など)がマスクに含まれた場合は、許可リストで検証してエラーにする。 - マスクにあるがボディにない、ボディにあるがマスクにない、存在しないパス、ネストしたフィールドをどう扱うか。
- マスクを必須にするか、省略を許すか(既存クライアントの互換性に関わります)。
2案の比較
| 対応1: AIP-126 大作戦 | 対応2: UpdateMask 大作戦 | |
|---|---|---|
| メリット | リクエストは通常の形のまま。実装が単純。 | 型を問わない。規約に従いつつ意図を伝えられる |
| デメリット | 列挙型にしか使えない。規約の対象から外す形。 | マスクとボディの二重管理。クライアントの負担。挙動の定義が必要 |
まとめ
いかがでしたか?今回はHTTP Request(PATCH)にフォーカスして、2つのアプローチを紹介しました。
ご紹介したアプローチはあくまで実装備忘録です。
みなさんのAPI開発においてなにか持ち帰れるものがあったのであれば幸いです。
方法は何でもよいと思うんですが、 「Request でリソースに対してやりたい操作が明確」ってことが良いAPIの条件 だと思ってます。
また、Style Guideは読むと得るものが結構多いので、箸休めに見てみるのもおすすめです。
ここまで読んでいただき、ありがとうございました。
※ AIPおよびGoogle JSON Style Guideの記述は、2026年9月時点で確認したものです。