Skip to content

← all backend comparisons

json modeling

Discriminated unions: serde tag vs json.RawMessage

One webhook endpoint, three event shapes (payment.succeeded, payment.failed, refund.created). Rust's serde reads the discriminator and lifts the right variant in one pass; Go has to do a two-step parse via an envelope of json.RawMessage fields.

Go (chi · sqlc · pgx)
go/internal/httpserver/webhook.go
// shop-two-backends not found at build time
Rust (axum · sqlx · tokio)
rust/src/routes/webhook.rs
// shop-two-backends not found at build time

What to take away

The headline is the type definition.

Rust: ten lines of enum WebhookEvent with #[serde(tag = "type")]. serde reads the "type" field, picks the variant, deserializes the remaining JSON into that variant's struct. The handler then match-es exhaustively — adding a fourth event variant refuses to compile until handled.

Go: an envelope struct with json.RawMessage for each possible payload field, a switch on env.Type, and a per-arm json.Unmarshal to lift the right sub-payload into a concrete struct. ~3× the lines, and the switch's default branch is the only thing standing between you and a silently-ignored event you forgot to handle.

Both backends drive the same state-machine transitions (payment.succeeded → paid, payment.failed → cancelled, refund.created → refunded) and return the same JSON shape on success — the parity test exercises all three event types per backend, plus the unknown event error path (which Rust normalizes from axum's default 422 down to 400 to match the Go side).