Skip to main content

第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…)💱


10-1. 「封筒」と「便箋」を分ける✉️📄

envelope 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

やること🛠️

  1. payloadのTypeScript型を書く🧩
  2. JSON Schemaを書く📄
  3. サンプル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貼り付け)