3 minutes
Advanced Pattern Matching Techniques in Ruby 3.x
Introduction
Ruby’s pattern matching (added in 2.7 and polished in 3.x) lets you concisely unpack and test data structures. It goes far beyond simple destructuring, guards, variable bindings, and custom class support open the door to DSLs, serializers, and cleaner conditionals.
In this post, we’ll explore advanced techniques:
- Deconstructing deeply nested objects
- Using guards and variable bindings to refine matches
- Pattern matching with Arrays, Hashes, and Structs
- Integrating with custom classes and DSLs
Nested Data Deconstruction
When you have a complex hash or array, pattern matching can pull out only what you need:
response = {
user: { id: 42, name: "Alex", stats: { posts: 10, comments: 5 } },
status: "ok"
}
case response
in { user: { id:, stats: { posts: posts_count } }, status: }
puts "User ##{id} made #{posts_count} posts (status: #{status})"
end
Here we:
- Match
response
against a shape. - Auto-assign
id
andposts_count
. - Ignore extra keys without boilerplate.
Guards and Variable Binding
You can add if
clauses inside in
to enforce extra checks:
case event
in { type: "purchase", amount: amount } if amount > 1000
puts "High-value purchase: $#{amount}"
end
This guard (if amount > 1000
) filters only big purchases. You can also bind to the whole object:
case data
in person @ { name:, age: } if age >= 18
send_adult_offer(person)
end
person @ { ... }
gives you the entire hash if it matches the pattern.
Pattern Matching with Arrays and Hashes
You’re not limited to fixed keys, you can match array prefixes, suffixes, and spreads:
case [1, 2, 3, 4]
in [first, *middle, last]
puts "Starts with #{first}, ends with #{last}, middle: #{middle.inspect}"
end
Hashes can match optional keys:
case opts
in { timeout: timeout, **rest }
puts "Timeout set to #{timeout}, other options: #{rest.keys.join(', ')}"
end
Structs and Custom Classes
If you’ve defined a Struct or class, Ruby can pattern-match it too:
User = Struct.new(:id, :email, :admin?)
case user
in User(id: id, admin?: true)
puts "Admin user ##{id} logged in"
end
For custom classes, define deconstruct
or deconstruct_keys
:
class Point
attr_reader :x, :y
def initialize(x, y); @x, @y = x, y; end
def deconstruct; [x, y]; end
end
case Point.new(3, 7)
in [x, y]
puts "Got point at (#{x},#{y})"
end
Real-world Use Cases in DSLs and Serializers
Pattern matching shines in Domain‑Specific Languages. For example, in a JSON serializer:
def serialize(obj)
case obj
in User(id:, name:)
{ type: "user", id: id, attributes: { name: name } }
in Article(**attrs)
{ type: "article", **attrs }
end
end
It makes your code terse and easy to extend for new types.
Conclusion
Ruby’s pattern matching in 3.x is more than syntactic sugar, it’s a powerful toolkit for writing clear, concise, and maintainable code. From deep data deconstruction to custom class matching, these features help you build DSLs and serializers with minimal fuss.
Experiment with guards, bindings, and custom deconstructors in your projects, and you’ll wonder how you ever managed without them!