rust 119 lines · 3 tabs

Tagged Webhook Deserialization and Typed Handler Dispatch in Rust with Serde

Shared by codesnips Aug 2026
3 tabs
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum WebhookEvent {
    #[serde(rename = "payment.succeeded")]
    PaymentSucceeded(PaymentSucceeded),

    #[serde(rename = "subscription.canceled")]
    SubscriptionCanceled(SubscriptionCanceled),

    #[serde(other)]
    Unknown,
}

#[derive(Debug, Deserialize)]
pub struct PaymentSucceeded {
    pub id: String,
    pub amount_cents: i64,
    pub currency: String,
    pub customer_id: String,
}

#[derive(Debug, Deserialize)]
pub struct SubscriptionCanceled {
    pub subscription_id: String,
    pub customer_id: String,
    #[serde(default)]
    pub reason: Option<String>,
}
3 files · rust Explain with highlit

This snippet shows how a webhook receiver in Rust decodes an untyped HTTP body into a strongly typed enum and routes each event to a dedicated handler. The core idea is to lean on Serde's internally tagged enum representation so that the type field in the payload selects the correct variant automatically, turning a runtime string match into a compile-time-exhaustive match.

In events.rs, the WebhookEvent enum uses #[serde(tag = "type", content = "data")] with rename_all so a body like {"type": "payment.succeeded", "data": {...}} deserializes straight into the matching variant. Each variant carries its own payload struct (PaymentSucceeded, SubscriptionCanceled, Unknown), which keeps the parsing logic honest: the data shape is validated against the type tag rather than being a loose serde_json::Value. The #[serde(other)] variant Unknown is important — it prevents a brand-new event type from failing deserialization outright, so the endpoint degrades gracefully instead of returning a hard error when a provider adds new events.

In dispatch.rs, EventDispatcher owns the typed handlers and exposes dispatch, which pattern-matches on the enum. Because the match is exhaustive over WebhookEvent, adding a new event variant forces the compiler to point at every place that must be updated, which is the main safety win of this approach over hand-written string routing. The handlers are async, and the Unknown arm simply logs and returns Ok(()) so unrecognized events are acknowledged rather than retried forever.

In handler.rs, the Axum route reads the raw body first, verifies the signature over those exact bytes with verify_signature, and only then calls serde_json::from_slice. Verifying before parsing matters: signatures are computed over the raw payload, so deserializing first would risk mismatches from reformatting. A malformed body returns 400, a bad signature returns 401, and a handler failure returns 500 so the provider retries. This layering — verify, deserialize, dispatch — is the standard shape for a reliable, typed webhook endpoint, trading a little boilerplate per event for strong guarantees and easy extension.


Related snips

ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
go
package deps

import (
  "net"
  "net/http"
  "time"

HTTP client tuned for production: timeouts, transport, and connection reuse

go http client
by Leah Thompson 1 tab
ruby
event_id = request.headers.fetch('X-Event-Id')
timestamp = request.headers.fetch('X-Signature-Timestamp').to_i

raise ActionController::BadRequest, 'stale request' if Time.now.to_i - timestamp > 300
raise ActionController::BadRequest, 'replay detected' if WebhookEvent.exists?(external_id: event_id)

Secure webhook endpoint design with replay protection

webhooks replay-protection hmac
by Kai Nakamura 1 tab

Share this code

Here's the card — post it anywhere.

Tagged Webhook Deserialization and Typed Handler Dispatch in Rust with Serde — share card
Link copied