Using Ruby Pattern Matching to Simplify Heterogeneous JSON Webhook Handling
Replace nested conditionals with declarative case/in patterns when processing varied webhook payloads in Ruby 3.x.
19 Jun 2026, 08:05 UTC

The problem: tangled conditionals for webhook events
Many applications receive webhook events that share a common envelope but differ in payload shape. A typical approach chains if/else statements, uses dig to safely fetch nested keys, and repeats key‑existence checks. As the number of event types grows, this code becomes hard to read, easy to miss a case, and brittle when the payload evolves.
Thesis: case/in pattern matching gives a declarative alternative
Ruby 3.x stabilised the case/in expression, allowing you to match against the structure of a value, bind parts to variables, and apply guards—all in one readable block. It replaces nested conditionals with a series of patterns that are tried in order, raising NoMatchingPatternError when none match unless an else clause is present.
Worked example: processing Stripe‑style webhook events
Suppose we receive a JSON payload that has been parsed into a Ruby hash. The payload always contains type (the event name) and data (an object whose shape varies by type). We want to handle three event kinds: charge.succeeded, invoice.payment_failed, and customer.subscription.updated.
require 'json'
payload = JSON.parse(<<~JSON)
{
"type": "charge.succeeded",
"data": {
"object": {
"id": "ch_123",
"amount": 2500,
"currency": "usd",
"billing_details": {
"email": "[contact removed]"
}
}
}
}
JSON
case payload
in { type: "charge.succeeded", data: { object: { id: charge_id, amount:, currency:, billing_details: { email: } } } }
puts "Charge #{charge_id} succeeded for #{email} (amount: #{amount} #{currency})"
in { type: "invoice.payment_failed", data: { object: { id: invoice_id, attempts: } } } when attempts > 3
puts "Invoice #{invoice_id} failed after #{attempts} attempts – escalate"
in { type: "customer.subscription.updated", data: { object: { id: sub_id, status: } } }
puts "Subscription #{sub_id} is now #{status}
else
puts "Unhandled event type: #{payload['type']}"
end
Each in line is a pattern:
- The first pattern matches a
charge.succeededevent, extracts the charge ID, amount, currency, and customer email, and prints a confirmation. - The second pattern matches an
invoice.payment_failedevent and adds a guard (when attempts > 3) so the block runs only after three or more retry attempts. - The third pattern matches a subscription update and binds the subscription ID and new status.
- The final
elseclause catches any payload that does not satisfy the preceding patterns, preventing aNoMatchingPatternErrorfrom bubbling up.
If you run this code with Ruby 3.2 or later, the appropriate block executes based on the type value, and the extracted variables are available locally within each branch.
Trade‑offs and limitations
- Subset matching by default. Hash patterns match only the keys you specify; extra keys are silently ignored. This is convenient for tolerant API consumption but can hide structural drift. To enforce an exact key set you must list every expected key or add a guard that checks the hash size.
- Exhaustiveness. Omitting the
elseclause makes non‑matching payloads raiseNoMatchingPatternError, which is useful when you want to fail fast for unknown event types. Remember to handle that exception if you prefer a graceful fallback. - Not a schema validator. Pattern matching assumes the incoming data is already well‑formed. For untrusted inputs or when you need to enforce types, ranges, or mandatory fields, combine it with a dedicated schema library (e.g.,
dry‑schemaorJSON‑Schema). - Performance. For typical application‑level workloads the overhead is negligible compared with the readability gain. Micro‑benchmarks show pattern matching is comparable to a series of
ifchecks; it is not intended to replace high‑throughput parsing loops.
Actionable closing
When you control the contract of incoming webhooks, start by replacing nested if/else blocks with a case/in expression. Use guards for additional conditions, leverage the else clause for explicit fallback handling, and remember that pattern matching works best alongside schema validation for data that originates outside your trust boundary. This approach yields code that is easier to read, simpler to extend, and less prone to missed cases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.