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:

  1. Match response against a shape.
  2. Auto-assign id and posts_count.
  3. 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!