JSON Webhook ペイロードから Rust 構造体へ

Stripe、GitHub、Slack のような Webhook イベントペイロードを Rust 構造体に変換します。type タグ enum、生データ通過、イベントディスパッチのパターンを扱います。

Real-World

詳細な説明

Webhook ペイロードを Rust でモデル化する

Webhook ペイロードは「メタデータを含むエンベロープ + イベント種別によって形が変わるボディ」という共通の形を取ります。serde の tag 属性を使えば、これを簡潔に表現できます。

サンプルペイロード(Stripe 風)

{
  "id": "evt_123",
  "type": "payment_intent.succeeded",
  "created": 1718447400,
  "data": {
    "object": {
      "id": "pi_456",
      "amount": 1999,
      "currency": "usd",
      "status": "succeeded"
    }
  }
}

初期生成構造体

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: String,
    #[serde(rename = "type")]
    pub event_type: String,
    pub created: i64,
    pub data: Data,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Data {
    pub object: Object,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Object {
    pub id: String,
    pub amount: i32,
    pub currency: String,
    pub status: String,
}

tag 付き enum に昇格

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum WebhookEvent {
    #[serde(rename = "payment_intent.succeeded")]
    PaymentIntentSucceeded(PaymentIntentBody),

    #[serde(rename = "invoice.payment_failed")]
    InvoicePaymentFailed(InvoiceBody),

    // 未知のイベント種別を捕捉するフォールバック
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentIntentBody {
    pub object: PaymentIntent,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentIntent {
    pub id: String,
    pub amount: i32,
    pub currency: String,
    pub status: String,
}

イベントのディスパッチ

match event {
    WebhookEvent::PaymentIntentSucceeded(body) => {
        confirm_order(&body.object.id).await?;
    }
    WebhookEvent::InvoicePaymentFailed(body) => {
        notify_finance(&body.object).await?;
    }
    WebhookEvent::Unknown => {
        log::warn!("Unrecognized webhook event");
    }
}

中継用の生データパススルー

サービスが Webhook をワーカーキューに転送するだけなら、ペイロードを serde_json::Value として保持し、二度のデコードを避けられます。

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawWebhook {
    pub id: String,
    #[serde(rename = "type")]
    pub event_type: String,
    pub raw: serde_json::Value,
}

署名検証

Stripe や GitHub の Webhook では、ボディをデコードするに必ず HMAC 署名ヘッダを検証してください。リシリアライズした文字列ではなく、リクエストの生バイトを使うこと。フォーマット差で署名不一致が起きるのを防ぐためです。

ユースケース

Stripe、GitHub、GitLab、Slack、Twilio など現代の SaaS プロバイダから来る Webhook を処理するシナリオに最適です。tag 付き enum によって、未知イベントをフォールバックハンドラへ流せる柔軟さを失わずに型安全なディスパッチが実現できます。

試してみる — JSON to Rust Struct Converter

フルツールを開く