第10章:payload設計のコツ(壊れにくいJSON)🧩📄
この章のゴール🎯
- 「受け側が困らない」イベントJSON(payload)を作れるようになる😊
- “あとから変更しても壊れにくい”設計のコツが身につく🛡️
- TypeScriptで「型」+「実行時バリデーション」まで一歩進める🚶♀️✨
10-1. まず「payload」と「封筒(envelope)」を分けよう📩📦
イベントには、だいたい2種類の情報が混ざります👇
- 封筒(envelope):イベントのメタ情報(誰が・いつ・何のイベント?)🪪🕒
- payload(中身):業務データ(注文ID・金額・ユーザーID…など)🛒💰
この2層を意識すると、設計が一気にスッキリします✨
標準フォーマットの代表例:CloudEvents☁️
CloudEvents は「イベントの封筒」の共通フォーマットで、specversion / id / source / type などの属性を持ちます。(GitHub)
**イメージ(封筒+dataがpayload)**👇
{
"specversion": "1.0",
"id": "01HZY9VQW1Y5T6N6C9D4K9NQ0A",
"source": "/orders",
"type": "order.confirmed.v1",
"time": "2026-02-03T06:00:00Z",
"datacontenttype": "application/json",
"data": {
"orderId": "ORD_123",
"customerId": "CUS_999",
"totalAmountMinor": 1200,
"currency": "JPY"
}
}
Outboxテーブルの
eventTypeは、CloudEventsだとtypeに相当する感じだよ🙂
10-2. eventType(イベント名)は「読みやすく・衝突しにくく」🏷️✨
ルール(おすすめ)📛
-
過去形っぽく(起きた事実)
- ✅
order.confirmed/ ✅user.registered
- ✅
-
ドメイン名を入れて衝突回避
- ✅
order.confirmed(注文系) - ✅
payment.succeeded(決済系)
- ✅
-
バージョンを必ず入れる(超重要)🧬
- ✅
order.confirmed.v1
- ✅
-
**単語区切りは
.(ドット)**が見やすい(好みでOK)🙂
よくある事故😵💫
- ❌
OrderConfirmed(言語ごとに表記揺れしやすい) - ❌
orderConfirm(“何が起きた?”が曖昧) - ❌ バージョン無し(あとで地獄👻)
10-3. フィールド設計の基本:「受け側が迷わない」🎁🙂
命名のコツ🧠
-
JSONのキーは camelCase が無難(TSとも相性◎)
- ✅
orderId,createdAt,totalAmountMinor
- ✅
-
“意味が1つ”になるように名前をつける
- ✅
totalAmountMinor(単位まで含めて確定!) - ❌
amount(円?ドル?税込?税抜?小数ある?😇)
- ✅
型のコツ🧷
-
IDは string(UUID/ULIDなど)
-
**金額は integer(最小通貨単位)**に寄せる(小数が事故りやすい)💰
- 例:$1.00 → 100(セント) / ¥100 → 100(円はゼロ小数通貨)
- こういう設計は決済APIでも一般的だよ。(docs.stripe.com)
-
**通貨コードは ISO 4217(3文字)**が定番(JPY/USD/EUR…)💱
- 公式説明。(iso.org)
10-1. 「封筒」と「便箋」を分ける✉️📄
Outboxパターンでは、データを 2つの層 で考えると整理しやすいです整理しやすいです📚
10-4. 「null」「欠損」「空文字」問題を片付けよう🧹😇
ここ、受け側が一番困るやつです…!🥹
基本方針(おすすめ)✅
-
“無い”は、基本「プロパティ自体を出さない」
- ✅
shippingAddressが無いならキーごと無し
- ✅
-
nullは極力使わない(意味がブレやすい)
null= 不明?未設定?削除済み?の区別が難しい💦
-
どうしても状態を表現したいなら、明示的なenumにする
- 例:
deliveryStatus: "unknown" | "scheduled" | "delivered"
- 例:
例(おすすめ)📦
{
"orderId": "ORD_123",
"couponCode": "SAVE10"
}
クーポン無しなら👇
{
"orderId": "ORD_123"
}
10-5. 日付・時刻は RFC 3339(文字列)で統一🕒🌍
分散システムでの時刻は「表記揺れ」が事故の元😵💫
RFC 3339 の形式(例:2026-02-03T06:00:00Z)がよく使われます。(IETF Datatracker)
TypeScript側はこういう型エイリアスを置くと安心感UP✨
type Rfc3339String = string; // 実際はバリデーションで守るよ
10-6. ID(event id / order id)は「並び替えたいならULIDも便利」🆔📌
イベントには 一意なIDが必須級です(重複排除・追跡に使う)🛡️ ULID は「文字列で扱いやすい」「時系列ソートしやすい」特徴があるよ。(GitHub)
ただし UUID でもOK!ここは “チームで統一” がいちばん強い💪
10-7. schemaVersion:payloadは将来変わる前提で守る🧬🛡️
payloadは、将来ほぼ確実に変わります🙂 だからどこかに バージョン情報を入れておくのが大事!
入れ方の例👇
typeに含める:order.confirmed.v1(シンプルで強い)- payloadに持つ:
schemaVersion: 1 - CloudEventsの
dataschemaにスキーマURLを入れる(大人なやり方)(GitHub)
10-8. 実行時バリデーション:JSON Schemaで「壊れたpayload」を止める🚧🧪
TypeScriptの型だけだと、受け取ったJSONが壊れてても通っちゃうことがあります😱 そこで JSON Schema を使うと「実行時に検査」できます。 JSON Schema(現行は 2020-12)が公式にまとめられています。(JSON Schema)
さらに、AsyncAPIでもメッセージpayloadをスキーマで定義する考え方が説明されています。(AsyncAPI)
例:OrderConfirmed の payload schema(学習用ミニ)📄
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "OrderConfirmedPayloadV1",
"type": "object",
"additionalProperties": false,
"required": ["orderId", "customerId", "totalAmountMinor", "currency", "confirmedAt"],
"properties": {
"orderId": { "type": "string" },
"customerId": { "type": "string" },
"totalAmountMinor": { "type": "integer", "minimum": 0 },
"currency": { "type": "string", "minLength": 3, "maxLength": 3 },
"confirmedAt": { "type": "string", "format": "date-time" }
}
}
TypeScriptの型(payload)🧩
export type OrderConfirmedPayloadV1 = {
orderId: string;
customerId: string;
totalAmountMinor: number; // integer想定
currency: string; // ISO 4217想定
confirmedAt: string; // RFC 3339想定
};
送る直前にバリデーション(イメージ)🔍
(ライブラリは何でもいいけど、ここでは雰囲気だけ🙂)
function assertValidOrderConfirmedPayload(input: unknown): asserts input is OrderConfirmedPayloadV1 {
// JSON Schema validator(例:Ajvなど)で検査する想定
// NGならthrowして「壊れたpayloadはOutboxに入れない」🚫
}
ポイント💡
- ✅ Outboxに保存する前に検査(壊れたイベントを貯めない)
- ✅
additionalProperties: falseで “変なキー混入” を防ぐ - ✅
requiredは慎重に(後方互換を考えると、増やしすぎ注意)
10-9. 追跡しやすさ:correlationId / traceparent を入れると最強🔍🧵
イベント駆動は「どの処理から来たイベント?」が迷子になりがち🥹 そこで 相関IDや トレース情報があると、ログ追跡がめちゃ楽になります✨
- W3C Trace Context の
traceparentは分散トレーシングで使われる標準的ヘッダーです。(W3C)
payloadに入れるより、封筒(envelope)側のメタに入れるのがおすすめ🙂 (このへんは第21章の「観測」で超効いてくるよ📊)
10-10. payload設計チェックリスト✅📝(この章のまとめ)
作ったpayloadを、送る前にこれでチェック🎀
- eventTypeは 過去形っぽく、意味が1つ?🏷️
- バージョンがどこかにある?🧬
- 金額は 最小通貨単位のinteger?💰
- 通貨は ISO 4217の3文字?💱
- 日付は RFC 3339(date-time文字列)?🕒
-
null地獄になってない?(欠損と区別できる?)😇 - 受け側が「これ何の単位?」ってならない?📏
- JSON Schema等で 実行時検査できる?🚧
- 余計な個人情報を入れてない?🔐
- ログ追跡できるメタ(correlation/trace)がある?🔍
練習問題🎓✨(手を動かすやつ)
お題:order.cancelled.v1 を設計しよう🛒❌
次を満たすpayloadを設計してみてね👇
- 必須:
orderId,cancelledAt,reasonCode reasonCodeは"customer_request" | "payment_failed" | "out_of_stock"のどれかcancelledAtは RFC 3339- 余計なキーは入れない(
additionalProperties: false)
やること🛠️
- payloadのTypeScript型を書く🧩
- JSON Schemaを書く📄
- サンプルpayloadを3つ作る(正常2つ+異常1つ)🧪
AI活用コーナー🤖✨(この章で使えるプロンプト例)
1) eventType候補を出してもらう🏷️
注文ドメインのイベントを設計中です。
「注文キャンセル」を表すeventTypeを、衝突しにくい命名で5案ください。
バージョン付き、ドット区切りでお願いします。
2) JSON Schemaのたたき台を作ってもらう📄
次のpayload要件を満たすJSON Schema Draft 2020-12 を生成して。
- orderId: string required
- cancelledAt: RFC3339 date-time required
- reasonCode: enum(customer_request, payment_failed, out_of_stock) required
- additionalProperties: false
3) “受け側が困る点” を指摘してもらう👀
このイベントpayload案をレビューして、受け側が困りそうな点を10個指摘して。
特に「null」「単位」「後方互換」「命名の曖昧さ」を厳しめに見て。
(payload貼り付け)