JSON Webhook Payload to Rust Struct

Convert webhook event payloads (Stripe, GitHub, Slack) into Rust structs. Patterns for type-tagged enums, raw passthrough, and event dispatch.

Real-World

Detailed Explanation

Modeling a Webhook Payload in Rust

Webhook payloads share a recurring shape: an envelope with metadata plus a polymorphic body whose schema depends on the event type. serde's tag attribute makes this trivial.

Example payload (Stripe-style)

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

Initial generated struct

#[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,
}

Promoted to a tagged 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),

    // Catch-all for unknown event types.
    #[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,
}

Dispatching events

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");
    }
}

Raw passthrough for forwarding

If your service merely forwards events to a worker queue, store the raw payload as serde_json::Value instead of decoding it twice:

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

Signature verification

For Stripe and GitHub webhooks, always verify the HMAC signature header before decoding the body. Use the raw bytes from the request (not a re-serialized version) to avoid signature mismatches caused by formatting differences.

Use Case

Processing webhooks from Stripe, GitHub, GitLab, Slack, Twilio, or any modern SaaS provider. Tagged enums give you type-safe dispatch without losing the ability to forward unknown events to a fallback handler.

Try It — JSON to Rust Struct Converter

Open full tool