Ruby Pattern Matching: Arrays, Hashes, and Data Extraction
Ruby pattern matching arrived in version 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.
TL;DR
Pattern matching in Ruby 3 combines destructuring, type checking, and variable binding into a single expression. Reach for case...in when you need to branch on the shape of arrays or hashes and extract values at the same time; use match? when a true/false answer is enough. Add guard clauses for extra conditions the pattern alone cannot express. It replaces nested conditionals and manual is_a? checks, especially when validating structured input such as API responses or command-line arguments.
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. This is fundamentally different from a regular case/when statement because the pattern can simultaneously check structure and populate local variables with the matched values.
Matching with match?
When you do not need the full branching power of case...in and only want a true or false answer, match? gives you a shorter alternative. It returns a boolean telling you whether the data fits the pattern you specified, without binding any variables or executing a branch body. 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. While match? works with any pattern, it is most commonly used with hash patterns for validating input shapes before processing them further.
How Ruby pattern matching works with arrays
Array patterns let you match against array structure, including rest patterns and type checking. When you destructure an array with a pattern, Ruby checks both the length and the elements, so you can express constraints that would otherwise require multiple lines of explicit guard code:
data = [1, 2, 3, 4, 5]
case data
in [first, *rest]
puts "First: #{first}, Rest: #{rest}"
end
# First: 1, Rest: [2, 3, 4, 5]
The *rest pattern captures everything after the first element into a new array. This is the pattern-matching equivalent of Ruby’s splat operator for destructuring, and it works on arrays of any length.
You can also match specific lengths and types by writing a pattern that describes exactly what you expect at each position:
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
When you match on types like Integer, String, or Boolean in an array pattern, Ruby checks each element’s class against the pattern position. This saves you from writing manual is_a? checks for every element in the array.
The | operator lets you match alternatives within a single pattern position, so you can express choices like “this element can be a 1 or a 2” without writing separate branches:
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
How pattern matching works with hashes
Hash patterns work similarly to array patterns but with key-value pairs, and they bring several advantages when you are dealing with structured data like API responses or configuration objects. The pattern syntax for hashes looks nearly identical to the hash literal syntax, which makes it easy to read once you have seen a few examples:
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
When you write { name:, email: } inside a pattern, Ruby checks that the hash has those keys and binds their values to local variables with the same names. The shorthand syntax keeps the pattern concise while extracting exactly the fields you need.
You can combine type checking with hash patterns to verify that each key holds the expected kind of value:
config = { host: "localhost", port: 443, ssl: true }
case config
in { host: String, port: Integer, ssl: Boolean }
puts "Valid configuration"
end
# Valid configuration
Typed hash patterns give you a one-pass check for both the presence of keys and the classes of their values. This is especially useful when validating configuration hashes where a missing key or a wrong type could cause subtle bugs downstream.
The **rest pattern captures remaining keys that you did not name explicitly, so you can extract the fields you care about while still keeping a reference to everything else:
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]
The **extra variable collects every key-value pair that was not listed in the pattern. This is handy when you are processing a response that carries metadata you may need later but do not want to spell out in the pattern.
Variable binding in pattern matching
Pattern matching automatically binds variables when patterns match, which means you do not need separate assignment statements after the check. This is one of its most powerful features:
point = [10, 20]
case point
in [x, y]
puts "Coordinates: (#{x}, #{y})"
end
# Coordinates: (10, 20)
The variables x and y are created and assigned automatically when the pattern matches. No explicit assignment is needed, and the variables are scoped to the branch body, so they do not leak into surrounding code.
You can use _ to ignore values you are not interested in, which keeps the pattern focused on the positions that matter:
data = [1, 2, 3]
case data
in [first, _, third]
puts "First: #{first}, Third: #{third}"
end
# First: 1, Third: 3
The underscore in a pattern is a placeholder that matches anything but binds nothing. This is the same convention Ruby uses for block parameters you plan to ignore, and it keeps the pattern readable by signaling which positions are irrelevant.
Ruby 3.1 introduced the pin operator (^) to reference previously bound variables inside a pattern, which lets you match against external values rather than creating new bindings:
name = "Alice"
case ["Alice", 30]
in [^name, age]
puts "Found #{name}, age #{age}"
end
# Found Alice, age 30
Without the pin, name inside the pattern would create a fresh variable binding instead of comparing against the outer name. The pin operator tells Ruby to use the existing value, making it possible to write patterns that check both shape and content.
Guard Conditions
Add extra conditions with if clauses when the structure alone is not enough to distinguish cases. Guard conditions let you apply arbitrary Ruby logic after the pattern matches, giving you a second layer of filtering before the branch executes:
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, so you can express constraints like “the sum of the first two elements must exceed 10” as part of the match. The guard runs only after the structural pattern succeeds, which keeps both checks in one readable location.
Using elsif with pattern matching
You can mix pattern matching with regular when clauses to handle cases where a traditional type check or value comparison is clearer than a full structural pattern. This flexibility means you can adopt pattern matching gradually in existing code without rewriting every branch:
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. A common pattern is to use in for structured payloads and when for simple scalar comparisons within the same case expression.
Returning values from pattern matches
The case expression returns the value of the matching branch, so you can use it for data extraction without assigning an intermediate variable. This turns the case statement into a transformer that takes structured input and produces a shaped output:
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, where the shape of the output depends on which branch matched. The else branch is a fallback that can return a default value or raise an error.
Practical use cases
Data Validation
Validate incoming data against expected structure and reject malformed payloads before they reach your business logic. Pattern matching gives you a declarative way to describe what valid data looks like:
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
By listing the expected keys, types, and allowed values in a single pattern, you get both validation and a clear specification of the data contract. The else branch catches anything that violates the expected shape without extra conditionals.
API response parsing
Cleanly handle different API response formats by matching on the response structure. Instead of writing nested if statements to check for error keys, data keys, or empty responses, you can describe each possible shape in a dedicated branch:
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"}
Each branch extracts the specific fields it needs and returns a uniform output shape, so the caller always gets back a predictable hash regardless of which response variant arrived. The => message syntax inside the error pattern captures the error string directly.
Control flow
Replace complex conditionals with pattern matching to make the structure of each command variant explicit. When you are building command-line tools or message handlers, each command shape becomes its own pattern:
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.
Key takeaways
- Pattern matching lets you describe the shape of the data you expect, not just the class of the object.
- Use
case...inwhen you want to branch on structure and extract values at the same time. - Use
match?when you only need a yes-or-no answer. - Guards help you combine structural checks with ordinary Ruby conditions.
- Pattern matching shines most when data is nested, irregular, or coming from an external source such as JSON.
Pattern matching versus case/when
Traditional case/when statements compare values. Pattern matching compares structure. That difference matters when you are validating arrays and hashes, or when you want to bind values while you check them.
For example, case response when { status: 200 } is not the same as case response in { status: 200 }. The in branch understands the hash layout and can bind additional values if you ask it to. That means you can turn a single branch into both a guard and a data extractor, which keeps your code shorter and easier to scan.
The old case/when style is still useful for simple equality checks, especially when you are matching strings, symbols, or a small set of constants. Pattern matching is the better choice when the data shape matters more than exact equality.
What are common pattern matching mistakes?
The most common mistake is using pattern matching when a simple predicate would be clearer. If you only need to ask whether a string is blank or a number is positive, a normal Ruby method is usually easier to read.
Another mistake is assuming every in branch will keep trying after one matches. It will not. The first matching branch wins, so order still matters. Put the most specific patterns first and keep fallback branches last.
It is also easy to confuse pattern variables with ordinary local variables. A pattern can bind a new value or compare against an existing one, depending on how you write it. If the code looks ambiguous, rename the variable or split the condition into two lines so the intent is obvious.
Finally, remember that pattern matching helps with shape, not with side effects. It does not validate business rules by itself. If your data has to satisfy extra rules such as date ranges or authorization checks, add those guards separately.
A concrete illustration helps. The pattern below looks correct but silently binds instead of comparing:
threshold = 5
case [3, 4]
in [threshold, b] # BINDS a new local 'threshold' to 3, does NOT compare
puts "threshold is now #{threshold}"
end
# threshold is now 3
The fix is the pin operator (^), which tells Ruby to use the outer variable for comparison rather than creating a fresh binding. Ruby borrowed the pin from Elixir, where the same operator distinguishes binding from matching. In practice, any variable you reference by name inside a pattern gets bound unless you pin it, so pin any value that should act as a constant during the match:
threshold = 5
case [3, 4]
in [^threshold, b] # ^ compares against outer threshold (5); no match here
puts "threshold is #{threshold}"
else
puts "No match -- 3 != 5"
end
# No match -- 3 != 5
Forgetting the pin is one of the most frequent surprises when moving from other languages with pattern matching, because Ruby defaults to binding rather than comparison.
Frequently asked questions
Is pattern matching faster than regular conditionals?
Usually the difference is small compared with the benefit in readability. Choose it for clarity first. If you are tuning a hot path, benchmark both versions and keep the one that fits the code best.
Can I use pattern matching with custom objects?
Yes, but you typically need to define deconstruct or deconstruct_keys so Ruby knows how to turn the object into a pattern-friendly shape. The official Ruby pattern matching documentation covers this in detail. That makes the object easier to work with in case...in branches.
Should I replace every case statement with pattern matching?
No. Pattern matching is excellent for nested arrays, hashes, and structured payloads, but plain case/when is still simpler for many everyday branches. Use the tool that makes the code easier to understand.
Conclusion
Ruby pattern matching gives you a clean way to combine validation and extraction in one readable construct. It is especially handy when you are dealing with API data, command parsing, or any other structured input where the shape matters. Once you get used to the syntax, you can replace a surprising amount of nested conditionals with a single, readable branch.
Start with simple Ruby pattern matches, add guards when the data needs extra rules, and keep your patterns close to the code that consumes the data. A well-placed ruby pattern often replaces several lines of manual type-checking and assignment.
A practical rule of thumb
Ruby pattern matching works best when the structure of the data matters more than a single yes-or-no check. If you are unpacking nested arrays and hashes, or you want to turn a response into a small Ruby object in one pass, case...in is usually a good fit. If you are just checking one value, a plain predicate or case/when branch is often simpler.
When you reach for it, keep the patterns short and let the branch body do the actual work. That keeps the code easy to scan and prevents the match expression from becoming a wall of punctuation.
See Also
- Ruby Working with Hashes — hash patterns depend on understanding hash keys and values
- Ruby Working with Arrays — array patterns apply the same destructuring principles
- Ruby Array Methods Guide — methods like
selectandrejectpair naturally with pattern matching - Ruby Blocks, Procs, and Lambdas — understanding closures helps when pattern matching passes blocks
- Ruby Metaprogramming Basics — useful background on runtime Ruby features