Pattern Matching in Ruby
Pattern matching arrived in Ruby 3.0, bringing a powerful way to destructure and validate data. Inspired by functional languages, it lets you match values against patterns—extracting data, checking structure, and controlling flow in a single expression. If you’ve used case statements but needed more power, pattern matching is what you’ve been looking for.
The Basic Match Syntax
Ruby provides two ways to use pattern matching: the case...in expression and the match method. The case...in form is most common:
data = [1, 2, 3]
case data
in [a, b, c]
puts "First: #{a}, Second: #{b}, Third: #{c}"
in []
puts "Empty array"
end
# First: 1, Second: 2, Third: 3
The in clause checks if the value matches the pattern. If it matches, any variables in the pattern are bound and the corresponding body executes.
Matching with match?
For simple boolean checks, use the match? method:
data = { name: "Alice", age: 30 }
data.match?(in { name:, age: }) # => true
data.match?(in { name: String, age: Integer }) # => true
data.match?(in { name: String, age: String }) # => false
This is useful for guard-like conditions without the full case expression.
Array Patterns
Array patterns let you match against array structure, including rest patterns and type checking:
data = [1, 2, 3, 4, 5]
case data
in [first, *rest]
puts "First: #{first}, Rest: #{rest}"
end
# First: 1, Rest: [2, 3, 4, 5]
You can also match specific lengths and types:
data = [1, "hello", true]
case data
in [Integer, String, Boolean]
puts "Matched three-element array with correct types"
end
# Matched three-element array with correct types
The | operator lets you match alternatives:
data = [1, 2]
case data
in [1 | 2, 3]
puts "First is 1 or 2, second is 3"
in [1, 2]
puts "First is 1, second is 2"
end
# First is 1, second is 2
Hash Patterns
Hash patterns work similarly to array patterns but with key-value pairs:
user = { name: "Alice", email: "alice@example.com", role: "admin" }
case user
in { name:, email: }
puts "Name: #{name}, Email: #{email}"
end
# Name: Alice, Email: alice@example.com
You can combine type checking with hash patterns:
config = { host: "localhost", port: 443, ssl: true }
case config
in { host: String, port: Integer, ssl: Boolean }
puts "Valid configuration"
end
# Valid configuration
The **rest pattern captures remaining keys:
response = { status: 200, data: { users: [] }, meta: { page: 1 } }
case response
in { status:, data: { users: }, **extra }
puts "Status: #{status}"
puts "Users: #{users}"
puts "Extra keys: #{extra.keys}"
end
# Status: 200
# Users: []
# Extra keys: [:meta]
Variable Binding
Pattern matching automatically binds variables when patterns match. This is one of its most powerful features:
point = [10, 20]
case point
in [x, y]
puts "Coordinates: (#{x}, #{y})"
end
# Coordinates: (10, 20)
You can use _ to ignore values you’re not interested in:
data = [1, 2, 3]
case data
in [first, _, third]
puts "First: #{first}, Third: #{third}"
end
# First: 1, Third: 3
Ruby 3.1+ introduced pin operator (^) to reference previously bound variables in a pattern:
name = "Alice"
case ["Alice", 30]
in [^name, age]
puts "Found #{name}, age #{age}"
end
# Found Alice, age 30
Guard Conditions
Add extra conditions with if clauses:
data = [1, 5]
case data
in [a, b] if a + b > 5
puts "Sum exceeds 5"
in [a, b]
puts "Sum is 5 or less: #{a + b}"
end
# Sum exceeds 5
This combines pattern matching with arbitrary Ruby logic.
Using elsif with Pattern Matching
You can mix pattern matching with regular when clauses:
value = 42
case value
in Integer if value > 10
puts "Large integer"
in String
puts "String: #{value}"
when NilClass
puts "Nil value"
else
puts "Unknown"
end
# Large integer
This flexibility lets you migrate code gradually or use the right tool for each branch.
Finding Patterns
The case expression returns the value of the matching branch, so you can use it for data extraction:
def extract_user(data)
case data
in { user: { name:, email: } }
{ name:, email: }
in [first, *rest]
{ name: first, items: rest }
else
{}
end
end
extract_user({ user: { name: "Bob", email: "bob@test.com" } })
# => {:name=>"Bob", :email=>"bob@test.com"}
This makes pattern matching excellent for parsing nested data structures.
Practical Use Cases
Data Validation
Validate incoming data against expected structure:
def validate_order(data)
case data
in {
id: Integer,
items: [_, *],
total: Float,
status: "pending" | "completed"
}
true
else
false
end
end
validate_order({ id: 1, items: ["A"], total: 29.99, status: "pending" })
# => true
validate_order({ id: 1, items: [], total: 0, status: "unknown" })
# => false
API Response Parsing
Cleanly handle different API response formats:
def handle_response(response)
case response
in { error: String => message }
{ type: "error", message: }
in { data: { users: [user, *] } }
{ type: "success", first_user: user }
in { data: [] }
{ type: "empty" }
else
{ type: "unknown" }
end
end
handle_response({ error: "Not found" })
# => {:type=>"error", :message=>"Not found"}
Control Flow
Replace complex conditionals with pattern matching:
def process_command(cmd)
case cmd
in ["login", username, password]
authenticate(username, password)
in ["logout"]
deauthenticate
in ["post", *content]
create_post(content.join(" "))
in ["exit" | "quit"]
exit
else
unknown_command(cmd)
end
end
process_command(["login", "alice", "secret"])
process_command(["post", "Hello", "world"])
This approach makes the structure of each command variant explicit and easy to read.
Summary
Pattern matching in Ruby 3 gives you structured destructuring, type checking, and conditional extraction in one powerful construct. Use case...in for full matching, match? for boolean checks, and combine with guards for complex conditions. It’s particularly valuable for validating API responses, parsing complex data, and replacing nested conditionals with clearer, more maintainable code.