RESTful APIリソース属性のデータモデリング:必須、任意、読み書き、デフォルト値の設計
はじめに
RESTful APIを設計する際、多くの場合、中心となるのは「リソース」です。ユーザー、商品、注文といったビジネス上のエンティティをリソースとして定義し、URIで識別し、HTTPメソッドを使って操作します。リソースそのものの定義に加え、そのリソースが持つ個々の「属性」(プロパティ、フィールド)をどのように設計するかも、APIの使いやすさ、堅牢性、保守性を大きく左右する重要な要素です。
特に、属性が「必須」なのか「任意」なのか、クライアントは「読み取り」だけ可能なのか「書き込み」も可能なのか、そして「デフォルト値」を持つのか、といった性質を明確に設計し、表現することは、クライアント開発者がAPIを誤りなく利用するために不可欠です。
本記事では、APIリソースの属性が持つこれらの性質に着目し、データモデリングの観点から、どのように設計し、APIのインターフェース(リクエスト/レスポンスボディ)で表現するのが適切かについて解説します。
属性の性質を明確にすることの重要性
APIリソースの各属性がどのような性質を持つのかを明確にすることは、以下のようなメリットをもたらします。
- クライアント側の実装容易性:
- どの属性が必須入力なのかが明確になり、クライアント側でのバリデーションやUI(入力フォームなど)の設計がしやすくなります。
- どの属性が読み取り専用なのかを知ることで、クライアントは不要な更新処理を行わずに済みます。
- デフォルト値がある場合、クライアントは値を送信しなくても問題ないことを理解できます。
- APIの堅牢性:
- サーバー側で必須属性の欠落や不正な書き込みを検知しやすくなり、APIの処理を堅牢に保つことができます。
- ドキュメントの正確性:
- 属性ごとの性質をスキーマ定義(例: OpenAPI)に盛り込むことで、APIドキュメントの精度が向上し、クライアント開発者との認識齟齬を防げます。
- 保守性と拡張性:
- 属性の性質が明確であれば、APIの変更(属性の追加、性質の変更など)を行った際の影響範囲を判断しやすくなります。
これらのメリットを享受するためには、データモデリングの段階で属性の性質を洗い出し、それをAPIのインターフェースに適切に反映させる必要があります。
属性の性質とデータモデリングにおける表現
APIリソースの属性が持ちうる主要な性質について、それぞれのデータモデリングの考え方と表現方法を見ていきましょう。
1. 必須属性 vs 任意属性
定義: * 必須属性: リソースの完全な状態を保つために、値が常に存在する必要がある属性。特にリソース作成時や更新時にクライアントからの指定が必須となる場合があります。 * 任意属性: 値が存在しなくてもリソースとして有効な属性。
データモデリング/表現:
* JSON Schema: JSON Schemaでは、オブジェクトのプロパティに対して、どのプロパティが必須かを required
キーワードで指定します。任意属性は required
に含めません。属性が null
を許容するかどうかは nullable
キーワードで指定できます(OpenAPI 3.0以降)。
* リクエストボディ:
* POST
(作成): 新しいリソースに必要な必須属性は、リクエストボディに含める必要があります。含めなかった場合は、サーバーはエラーレスポンスを返します。
* PUT
(全更新): リソースの現在の状態を完全に置き換えるため、多くの場合、元のリソースの必須属性を含めた全ての属性(サーバー側で自動設定されるものを除く)をリクエストボディに含める必要があります。
* PATCH
(部分更新): リソースの一部属性のみを更新するため、通常、必須属性も含め、リクエストボディの全ての属性は「任意」として扱われます。ただし、特定の操作においては、PATCH
でも特定の属性が必須となる場合もあります。
* レスポンスボディ: レスポンスボディに含まれる属性は、原則としてサーバー側で値が設定されているものですが、データが存在しない場合(例: 関連リソースがない)や、その属性がデータモデル上任意である場合は、JSON上でキーを含めない、または値を null
とすることがあります。API設計では、任意属性で値が存在しない場合にキーを含めるか含めないかのルールを定めておくと一貫性が保てます。
例: ユーザーリソース (/users
)
// GET /users/123 レスポンスボディ例
{
"id": "user-123",
"name": "山田太郎", // 必須
"email": "ichiro.yamada@example.com", // 必須
"createdAt": "2023-10-27T10:00:00Z", // 必須(サーバー生成)
"bio": "ソフトウェアエンジニアです。", // 任意
"website": null // 任意 (値がない場合はnullまたはキーを含めない)
}
// POST /users リクエストボディ例
{
"name": "山田太郎", // 必須
"email": "ichiro.yamada@example.com", // 必須
"bio": "ソフトウェアエンジニアです。" // 任意
// createdAt, id はサーバー生成のため含めない
}
この例では、name
と email
はユーザー作成に必須であり、レスポンスにも常に含まれる属性です。bio
と website
は任意属性であり、リクエストに含めなくてもよく、レスポンスでも値が存在しない場合は null
やキーの省略が許容されます。
2. 読み取り専用属性 vs 書き込み可能属性
定義: * 読み取り専用属性: クライアントが値を変更することはできない属性。通常、サーバーによって自動的に設定されるか、他のデータソースから派生する値です。 * 書き込み可能属性: クライアントが値を指定または変更できる属性。
データモデリング/表現:
* JSON Schema: JSON Schema(特にOpenAPIの文脈でよく使用される拡張)では、属性に対して readOnly: true
や writeOnly: true
を指定できます。
* リクエストボディ: PUT
/POST
/PATCH
リクエストボディには、書き込み可能な属性のみを含めるのが原則です。読み取り専用属性をリクエストボディに含めても、サーバー側ではその値を無視するか、あるいはエラーとすべきです。意図しないデータ変更を防ぐために、読み取り専用属性はリクエストボディから除外する設計が望ましいです。
* レスポンスボディ: レスポンスボディには、クライアントが参照できる全ての属性を含めます。これには読み取り専用属性と書き込み可能属性の両方が含まれます。
例: ユーザーリソース (/users
)
上記の例で挙げた属性を再度考えます。
// GET /users/123 レスポンスボディ例
{
"id": "user-123", // 読み取り専用
"name": "山田太郎", // 書き込み可能
"email": "ichiro.yamada@example.com", // 書き込み可能 (変更可能な場合)
"createdAt": "2023-10-27T10:00:00Z", // 読み取り専用
"updatedAt": "2023-10-27T10:30:00Z", // 読み取り専用
"bio": "ソフトウェアエンジニアです。" // 書き込み可能
}
// PUT /users/123 リクエストボディ例
{
"name": "山田 太郎", // 書き込み可能
"email": "ichiro.yamada+new@example.com", // 書き込み可能
"bio": "新しい自己紹介文です。", // 書き込み可能
// id, createdAt, updatedAt は読み取り専用のため含めない
}
id
, createdAt
, updatedAt
のようなサーバーが管理する属性は、クライアントからは変更できないため読み取り専用とします。name
, email
, bio
はクライアントが更新できる書き込み可能属性です。これらの性質を明確にすることで、例えばクライアントが誤って id
をリクエストボディに含めて送信しても、サーバーがそれを無視することが期待でき、安全性が高まります。
3. デフォルト値を持つ属性
定義: * デフォルト値を持つ属性: クライアントが値を指定しなかった場合に、サーバー側で事前に定義されたデフォルト値が自動的に設定される属性。
データモデリング/表現:
* JSON Schema: JSON Schemaでは default
キーワードでデフォルト値を指定できます。
* リクエストボディ: クライアントはデフォルト値を利用したい場合、その属性をリクエストボディに含めません。明示的に異なる値を設定したい場合にのみ、リクエストボディに含めます。
* レスポンスボディ: レスポンスボディには、実際にリソースに設定されている値が含まれます。これはクライアントが指定した値か、指定がなかった場合のデフォルト値のいずれかです。
例: ユーザー設定リソース (/user-settings/{id}
)
// GET /user-settings/123 レスポンスボディ例
{
"id": "setting-123",
"userId": "user-123", // 読み取り専用
"notificationsEnabled": true, // デフォルト値はtrue。クライアントが変更可能
"language": "ja", // デフォルト値は"en"。クライアントが変更可能
"theme": "dark" // デフォルト値なし。クライアントが指定可能。
}
// POST /user-settings リクエストボディ例 (languageを明示的に指定)
{
"userId": "user-123",
// notificationsEnabled はデフォルト値 (true) を利用するため含めない
"language": "ja"
// theme はデフォルト値なし、今回指定しない(任意)
}
この例では、notificationsEnabled
のデフォルト値は true
、language
のデフォルト値は "en"
とします。クライアントが設定リソースを新規作成する際に notificationsEnabled
をリクエストボディに含めなければ、サーバーは自動的に true
を設定します。language
も同様で、含めなければ "en"
となります。theme
にはデフォルト値がないため、クライアントが値を指定しない場合はサーバー側で別のルールに従って処理するか(例: nullを格納、またはエラー)、あるいは必須属性として扱うかなどを別途定義する必要があります。
複合的な性質を持つ属性の設計
属性の性質は、単一の性質を持つだけでなく、複数の性質を組み合わせたり、API操作(HTTPメソッド)によって変化したりすることがあります。
例えば、PATCH /users/{id}
のような部分更新操作では、リクエストボディに含まれる全ての属性は「任意」として扱われるのが一般的です。本来必須であるはずの name
属性も、PATCH
リクエストにおいては更新したい場合にのみ含めればよく、含めなかった場合は既存の値が維持される、という設計が一般的です。しかし、更新操作の内容によっては、特定の属性(例えばパスワード変更時の newPassword
)が PATCH
リクエストでも必須となる場合もあります。
このような操作による属性の性質の変化は、APIドキュメントで明確に記述する必要があります。OpenAPIなどのスキーマ定義言語を活用することで、パスやHTTPメソッドごとに異なるリクエストボディのスキーマを定義し、属性の必須性や読み書き権限を詳細に記述することが可能です。
アンチパターン
属性の設計におけるよくあるアンチパターンとその影響を理解しておくことも重要です。
- 属性の性質がドキュメント化されていない: クライアント開発者はAPIの挙動を推測するしかなくなり、実装ミスや問い合わせが増加します。
- 必須属性なのに
null
を許容してしまう: データ整合性が損なわれるリスクがあります。データモデル上はnull
不可なのに、APIの入力バリデーションが甘い場合に発生しがちです。 - 読み取り専用属性をリクエストボディで受け付け、無視する: クライアント側は変更できたと誤解する可能性があります。サーバー側でエラーとするか、そもそもリクエストボディに含めないよう設計するのが望ましいです。
- デフォルト値があるのにドキュメントに記載がない: クライアントは値を省略した場合の挙動を理解できず、意図しない結果を招く可能性があります。
- 操作によって属性の性質が変わるが、それが不明確: 例:
PUT
では必須だった属性がPATCH
では任意になることをクライアントが知らないなど。APIの利用方法が混乱します。
これらのアンチパターンは、APIの使いにくさや不具合の原因となります。属性一つ一つの性質を丁寧に洗い出し、意図した通りにAPIインターフェースに反映させることが重要です。
OpenAPIスキーマによる明確化
APIの属性定義を正確かつ機械判読可能な形式で表現するために、OpenAPIのようなAPIスキーマ定義言語を活用することは非常に効果的です。
OpenAPI 3.0以降では、スキーマオブジェクトのプロパティ定義において、属性の性質を表現するための様々なキーワードが用意されています。
components:
schemas:
User:
type: object
properties:
id:
type: string
format: uuid
description: ユーザーID (サーバー生成)
readOnly: true # 読み取り専用
name:
type: string
description: ユーザー名
minLength: 1
email:
type: string
format: email
description: メールアドレス
createdAt:
type: string
format: date-time
description: 作成日時 (サーバー生成)
readOnly: true # 読み取り専用
bio:
type: string
description: 自己紹介文
nullable: true # nullを許容する(任意属性として設計する場合)
required: # 必須属性のリスト
- id
- name
- email
- createdAt
UserSettings:
type: object
properties:
id:
type: string
readOnly: true
userId:
type: string
readOnly: true
notificationsEnabled:
type: boolean
description: 通知を有効にするか
default: true # デフォルト値
language:
type: string
description: 表示言語コード
default: "en" # デフォルト値
enum: # 取りうる値のリスト(これも一種の制約)
- "en"
- "ja"
- "fr"
上記のように readOnly
, nullable
, default
, required
といったキーワードを適切に使うことで、APIドキュメントの自動生成ツールが正確なドキュメントを作成できますし、クライアントSDKの生成、サーバー側のバリデーションコード生成などにも役立ちます。これは、クライアントとサーバー間の「契約」としてのAPI定義の信頼性を高めることに繋がります。
考慮事項
- バージョニング: APIのバージョンアップに伴い、属性が必須から任意に変わる、あるいはその逆になることがあります。特に任意から必須への変更は既存クライアントの後方互換性を損なう可能性が高い変更です。変更の影響を慎重に評価し、必要に応じてAPIバージョニング戦略を検討する必要があります。
- ネストされたオブジェクト: 属性がネストされたオブジェクトである場合、そのネストされたオブジェクト自体の必須性や、その中のプロパティの必須性/任意性も同様に設計・定義する必要があります。
- コンテキスト依存の必須性: 特定の条件や、リクエストボディ内の他の属性の値に応じて、ある属性が必須になる場合があります(例:
paymentMethod
が"credit_card"
の場合にcardNumber
が必須になるなど)。このような複雑な依存関係もAPIドキュメントで明確に説明する必要があります。
まとめ
RESTful APIにおけるデータモデリングは、リソースの定義だけでなく、リソースを構成する個々の属性(プロパティ)の設計にまで踏み込むことで、より高品質なAPIを実現できます。属性が「必須」なのか「任意」なのか、「読み取り専用」なのか「書き込み可能」なのか、「デフォルト値」を持つのか、といった性質を明確に定義し、それをAPIインターフェース(リクエスト/レスポンスボディ)に適切に反映させることは、APIの使いやすさ、堅牢性、そして長期的な保守性に不可欠です。
特に、JSON Schemaを用いたAPIスキーマ定義は、これらの属性の性質を正確に表現し、クライアントとサーバー間の共通理解を深める強力なツールとなります。設計段階で属性の性質を丁寧に洗い出し、APIドキュメントやスキーマ定義を通じて関係者間で共有することで、より信頼性の高いAPI開発を進めることができるでしょう。