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.
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.