Ruby

Ruby Pattern Matching: case/in, Find Patterns, and the Pin Operator

Ruby Pattern Matching: case/in, Find Patterns, and the Pin Operator

Pattern matching in Ruby checks a value against a structure and binds the parts you name to local variables. It runs through case/in (branches, optional else) or standalone: value => pattern raises NoMatchingPatternError on a mismatch, value in pattern returns true or false. Patterns cover arrays, hashes, classes, ranges, and your own objects via deconstruct/deconstruct_keys.

This guide covers each pattern type, the pin operator, deconstruct_keys for your own classes, and the exact messages a failed match produces. Every example and transcript here ran on Ruby 3.4.10. If you landed here mid-debug, jump straight to the errors section.

Ruby Pattern Matching with case/in

A case/in expression lists patterns in order. Ruby tests the value against each in branch until one matches, runs that branch’s body, and returns its value:

Ruby
def describe(value)
  case value
  in Integer
    "an integer"
  in String
    "a string"
  else
    "something else"
  end
end
 
describe(1) # => "an integer"
describe("one") # => "a string"
describe(:one) # => "something else"

The first match wins, and Ruby never looks at the branches after it, so put specific branches before general ones:

Ruby
case 1
in Integer
  "first"
in 1
  "second"
end
# => "first"

The else branch is optional. Without it, the match has to succeed: a value that fits no branch raises NoMatchingPatternError (the exact messages are in the errors section). case/in and case/when are separate constructs, so a when and an in cannot share one case; mixing them is a SyntaxError (unexpected 'in', ignoring it).

Value, Alternative, and Range Patterns

The simplest pattern is a value. Ruby tests it with ===, the same operator case/when uses, so anything that works after when works after in: a class checks Integer === value, a range checks membership, a regexp matches the string and sets $1, and a lambda is called with the value. nil matches only nil (so does NilClass). A | between patterns builds an alternative that matches if either side does, and => name after a pattern binds whatever matched:

Ruby
def size_of(n)
  case n
  in nil
    "no value"
  in ..0
    "zero or less"
  in 1 | 2 | 3
    "small"
  in (10..)
    "ten or more"
  in Integer | Float => other
    "in between: #{other}"
  end
end
 
size_of(nil) # => "no value"
size_of(-3) # => "zero or less"
size_of(2) # => "small"
size_of(42) # => "ten or more"
size_of(7) # => "in between: 7"
size_of(4.5) # => "in between: 4.5"

The parentheses around the endless range are not decoration. Written as in 10.. at the end of a line, Ruby reads the next line as the range’s end, and 10.."ten or more" fails at runtime with bad value for range (ArgumentError).

Regexps and lambdas cover the checks that have no literal form:

Ruby
def version_of(name)
  case name
  in /\Aruby-(\d)/
    "Ruby #{$1}"
  in ->(s) { s.start_with?("jruby") }
    "JRuby"
  in String
    "unknown"
  end
end
 
version_of("ruby-3.4") # => "Ruby 3"
version_of("jruby-10.0") # => "JRuby"
version_of("crystal") # => "unknown"

A lambda pattern calls the lambda with the value and matches when the result is truthy; our introduction to lambdas covers the -> syntax.

Pattern Matching Arrays in Ruby

An array pattern matches an array by length, then each element by a pattern of its own: a class, a literal, or a name to bind. All four of these branches match [1, 2, "Three"]; only the first one runs, because case stops at the first match:

Ruby
case [1, 2, "Three"]
in [Integer, Integer, String]
  "matches"
in [1, 2, "Three"]
  "matches"
in [Integer, *]
  "matches" # * matches any number of elements, including none
in [a, *]
  "matches" # and the value of the variable a is now 1
end

Without a splat, the length must match exactly. A named splat binds the rest of the array, [] matches only an empty array, and a value that is not an array (anything without a deconstruct method) fails the pattern without raising:

Ruby
value = [1, 2, "Three"]
 
value in [Integer, Integer] # => false
value in [Integer, *rest] # => true
rest # => [2, "Three"]
[] in [] # => true
5 in [Integer] # => false

Array patterns shine when a method can return more than one kind of result. Elixir code returns {:ok, value} and {:error, reason} tuples for exactly this reason, and the idiom reads as well in Ruby: a save helper returns [:ok, model] or [:error, errors], and the caller matches on the tag.

This pattern reads well in a Rails-style controller action:

Ruby
def create
  case save(model_params)
  in [:ok, model]
    render :json => model
  in [:error, errors]
    render :json => errors
  end
end
 
# Somewhere in your code, e.g. inside a global helper or your model base class (with a different name).
def save(attrs)
  model = Model.new(attrs)
  model.save ? [:ok, model] : [:error, model.errors]
end

The Find Pattern

A find pattern has a splat at both ends and matches the first element (or run of elements) between them that fits, wherever it sits in the array:

Ruby
case [1, "two", :three, 4.0]
in [*, Symbol => sym, *]
  sym
end
# => :three

Name the splats to capture what came before and after the match:

Ruby
case [1, "two", :three, 4.0]
in [*before, String => str, *after]
  [before, str, after]
end
# => [[1], "two", [:three, 4.0]]

The pattern in the middle can be anything, including a hash pattern, which turns the find pattern into a compact search through a list of records:

Ruby
events = [{type: "click"}, {type: "error", code: 500}, {type: "view"}]
 
case events
in [*, {type: "error", code:}, *]
  code
end
# => 500

The find pattern arrived in Ruby 3.0 as experimental and has been stable since 3.2; Ruby 3.4 prints no warning for it.

Pattern Matching Hashes in Ruby

A hash pattern matches on symbol keys and, for each key, a pattern for its value. Hash patterns match partially by default: extra keys in the value are ignored unless the pattern ends in **nil. Every branch here except the last matches {a: 1, b: 2}:

Ruby
case {a: 1, b: 2}
in {a: Integer}
  "matches" # By default, all hash matches are partial
in {a: Integer, **}
  "matches" # and is the same as {a: Integer}
in {a: a}
  "matches" # and the value of variable a is now 1
in {a: Integer => a}
  "matches" # and the value of variable a is now 1
in {a: 1, b: b}
  "matches" # and the value of variable b is now 2
in {a: Integer, **nil}
  "does not match" # This matches only if the hash has a and no other keys
end

Three more forms come up constantly. {a:} binds a to the value at :a without repeating the name, **rest collects the keys the pattern did not mention, and {} is the one non-partial hash pattern: it matches only an empty hash. String keys never match a hash pattern, so a JSON.parse result needs symbolize_names: true first (the details are under expected a label as the key in the hash pattern):

Ruby
value = {a: 1, b: 2}
 
value in {a:} # => true
a # => 1
value in {a: Integer, **rest} # => true
rest # => {b: 2}
value in {} # => false
{} in {} # => true
{"a" => 1} in {a: 1} # => false

At the top level of a pattern the braces are optional: in db: {user:} matches {db: {user: "admin"}} and binds user to "admin".

Hash patterns work well for enforcing the shape of incoming params. A greeter with strongly opinionated arguments looks like this:

Ruby
def greet(hash = {})
  case hash
  in {greeting:, first_name:, last_name:}
    greet(greeting:, name: "#{first_name} #{last_name}")
  in {first_name:, last_name:}
    greet(greeting: "Hello", first_name:, last_name:)
  in {greeting:, name:}
    puts "#{greeting}, #{name}"
  in {name:}
    greet(greeting: "Hello", name:)
  in {greeting:}
    greet(greeting:, name: "Anonymous")
  else
    greet(greeting: "Hello", name: "Anonymous")
  end
end
 
greet
greet(name: "John")
greet(first_name: "John", last_name: "Doe")
greet(greeting: "Bonjour", first_name: "John", last_name: "Doe")
greet(greeting: "Bonjour")
Shell
$ ruby greeter.rb
Hello, Anonymous
Hello, John
Hello, John Doe
Bonjour, John Doe
Bonjour, Anonymous

Because hash patterns are partial, branch order carries the logic. The three-key branch has to come before {first_name:, last_name:}: swapped, the two-key branch would match every call that carries both names, including its own recursive call, and the method would recurse until stack level too deep (SystemStackError). The greeting: shorthand in the recursive calls is Ruby 3.1’s keyword-argument shorthand, the same idea as {greeting:} in a pattern.

Variable Binding and Pinning in Ruby

Binding is what makes pattern matching more than a type check: any bare name inside a pattern becomes a local variable holding the part it matched. It also means a bare name never compares. Even when a variable with that name already exists, the pattern rebinds it:

Ruby
expected = 1
 
case 99
in expected
  "matched"
end
# => "matched"
 
expected # => 99

The pattern in expected matches anything and overwrites expected with 99. To compare against the existing value, pin it, as the next section shows.

Bind a name four ways:

  1. With a type check, e.g. in [Integer => a] or in {a: Integer => a}.
  2. Without the type check, e.g. in [a, 1, 2] or in {a: a}.
  3. Without the variable name, which defaults to the key name, e.g. in {a:} defines a variable named a with the value at key a.
  4. As the rest, e.g. in [Integer, *rest] or in {a: Integer, **rest}.
Ruby
[5] in [Integer => a]
a # => 5
[9, 1, 2] in [b, 1, 2]
b # => 9
{c: 8} in {c:}
c # => 8
[1, 2, 3] in [Integer, *rest]
rest # => [2, 3]
{d: 1, e: 2} in {d: Integer, **others}
others # => {e: 2}

The Pin Operator ^

Prefix a name with ^ to compare against the variable’s current value instead of rebinding it:

Ruby
a = 1
 
case {a: 1, b: 2}
in {a: ^a}
  "matches"
end
# => "matches"

The pin also works on a variable bound earlier in the same pattern, which is how you say “these two parts must be equal”:

Ruby
order = {billing_address: {city: "Amsterdam"}, shipping_address: {city: "Amsterdam"}}
 
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  "both billing and shipping are to the same city"
else
  "both billing and shipping must be to the same city"
end
# => "both billing and shipping are to the same city"

With "Zurich" as the shipping city, the same expression takes the else branch. The same trick fixes a common SyntaxError: in [x, x] is rejected as a duplicated variable name, while in [x, ^x] matches two equal elements.

Since Ruby 3.1, the pin takes more than a local variable. ^(expression) pins the result of any expression, including a method call, and ^@ivar and ^$gvar pin instance and global variables directly:

Ruby
config = Struct.new(:role).new(:admin)
@limit = 2
$env = "production"
 
case {role: :admin, retries: 2, env: "production"}
in {role: ^(config.role), retries: ^@limit, env: ^$env}
  "all three pinned"
end
# => "all three pinned"

The parentheses around a method call are required: ^config.role is a SyntaxError (unexpected '.', ignoring it), because the grammar allows only a variable or a parenthesized expression after ^. Write ^(config.role).

One behavior not to rely on: a branch that fails may have bound variables before the mismatch was found. The 2021 version of this post demonstrated it, and the result reproduces on Ruby 3.4.10:

Ruby
city = "Amsterdam"
order = {billing_address: {city: "Berlin"}, shipping_address: {city: "Zurich"}}
 
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  puts "both billing and shipping are to the same city"
else
  puts "both billing and shipping must be to the same city"
end
 
puts city
Shell
$ ruby failed_branch.rb
both billing and shipping must be to the same city
Berlin

The pattern bound city to "Berlin" from the billing address, then failed the pin check on the shipping address, and the binding stayed. Ruby’s pattern matching reference lists “use of a variable in an unmatched pattern” in its appendix of undefined behavior, kept undefined to leave room for future optimization. Treat a variable bound inside a pattern as valid only in that branch’s body, or after a => or in that succeeded; never read what a failed branch left behind.

Matching Ruby's Custom Classes

Any object can take part in pattern matching by implementing two methods. deconstruct_keys(keys) returns a hash and makes the object match hash patterns; deconstruct returns an array and makes it match array patterns. Ruby calls them only when a pattern of that shape is tested against the object.

To match a user against their first_name and last_name, define deconstruct_keys:

Ruby
class User
  attr_reader :first_name, :last_name
 
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
 
  def deconstruct_keys(keys)
    {first_name: first_name, last_name: last_name}
  end
end
 
user = User.new("John", "Doe")
 
case user
in {first_name: "John"}
  "Hey, John"
end
# => "Hey, John"

The keys argument is the array of keys the pattern asked for, [:first_name] here, so an object whose attributes are expensive to compute can return only those. When the pattern contains **rest, Ruby passes nil instead, because it needs every key; return the full hash in that case.

deconstruct does the same for array patterns. A Location that exposes itself as [latitude, longitude]:

Ruby
class Location
  attr_reader :latitude, :longitude
 
  def initialize(latitude, longitude)
    @latitude = latitude
    @longitude = longitude
  end
 
  def deconstruct
    [latitude, longitude]
  end
end
 
case Location.new(52.37, 4.89)
in [Float => latitude, Float => longitude]
  "#{latitude}, #{longitude}"
end
# => "52.37, 4.89"

Both methods have a return-type contract: a deconstruct that returns anything but an array raises deconstruct must return Array (TypeError), and a deconstruct_keys that returns anything but a hash raises deconstruct_keys must return Hash (TypeError).

To check the class as well as the shape, put the constant in front of the pattern: User(first_name: "John") is the hash form and Location[lat, lng] the array form. Ruby tests User === value first and calls deconstruct_keys only when that passes, so a value of the wrong class falls through without the method ever running:

Ruby
case user
in User(first_name: "John")
  "a User called John"
end
# => "a User called John"
 
case "not a user"
in User(first_name:)
  "never reached"
else
  "User === value failed first"
end
# => "User === value failed first"

Struct, Data, and Other Built-In Deconstructors

Struct classes define both methods already, so a struct matches array and hash patterns with no extra code:

Ruby
Point = Struct.new(:x, :y)
point = Point.new(1, 2)
 
point.deconstruct # => [1, 2]
point.deconstruct_keys([:x]) # => {x: 1}
point.deconstruct_keys(nil) # => {x: 1, y: 2}
 
case point
in Point(x: 1, y:)
  y
end
# => 2

Data.define, the immutable value class added in Ruby 3.2, does the same whether you construct it positionally or with keywords:

Ruby
Coord = Data.define(:lat, :lng)
coord = Coord.new(lat: 52.37, lng: 4.89)
 
case coord
in Coord[lat, lng]
  [lat, lng]
end
# => [52.37, 4.89]
 
case coord
in Coord(lat: Float => lat)
  lat
end
# => 52.37

Ruby 3.2 also gave MatchData, Time, and Date a deconstruct_keys, so named captures and calendar fields match directly:

Ruby
"2026-09-09".match(/(?<year>\d+)-(?<month>\d+)/) => {year:, month:}
[year, month] # => ["2026", "09"]
 
case Time.new(2026, 9, 9)
in {year: 2026, month:}
  month
end
# => 9
 
require "date"
 
case Date.new(2026, 9, 9)
in {year:, yday:}
  [year, yday]
end
# => [2026, 252]

Using Guards for Complex Patterns

When a condition cannot be expressed as a pattern, add a guard: an if or unless after the pattern. The guard sees the variables the pattern bound and runs only when the pattern itself matched. A failed guard counts as a failed match, and Ruby moves on to the next branch:

Ruby
case [1, 2]
in [a, b] if b == a * 2
  "matches"
else
  "no match"
end
# => "matches"
 
case [1, 3]
in [a, b] unless b == a * 2
  "not a double"
end
# => "not a double"

Pattern Matching with =>/in Without case

A single pattern needs no case. value => pattern matches and binds, or raises NoMatchingPatternError:

Ruby
[1, 2, "Three"] => [Integer => one, two, String => three]
 
one # => 1
two # => 2
three # => "Three"

value in pattern tests the same pattern but returns true or false and never raises, not even for a missing hash key:

Ruby
config = {role: :user}
 
config in {role: :admin} # => false
config in {role: Symbol => role} # => true
role # => :user
{name: "Ada"} in {email:} # => false

The two forms are not interchangeable. => produces no value at all (Ruby calls it a void value expression), so it suits unpacking data whose shape you trust, where a mismatch is a bug worth raising on. in is a boolean, so it suits a condition. Assigning the result of => fails at parse time:

Shell
$ ruby -e 'result = ({a: 1} => {a:})'
-e: -e:1: syntax error found (SyntaxError)
> 1 | result = ({a: 1} => {a:})
    |           ^~~~~~~~~~~~~~ unexpected void value expression

And a mismatch on => raises with a message that names the failing check:

Shell
$ ruby -e '{role: :user} => {role: :admin}'
-e:1:in '<main>': {role: :user}: :admin === :user does not return true (NoMatchingPatternError)

Since Ruby 3.1, the pattern on the right of => or in can drop its outer brackets:

Ruby
[0, 1] => _, x
x # => 1

Because => raises, it makes a compact assertion. A base controller that admits only admins could check the current user’s role in one line:

Ruby
class AdminController < AuthenticatedController
  before_action :verify_admin
 
  private
 
  def verify_admin
    Current.user => {role: :admin}
  rescue NoMatchingPatternError
    raise NotAllowedError
  end
end

One prerequisite: Current.user must respond to deconstruct_keys, whether through a Struct, a Data, or a method you wrote. An object without it never matches a hash pattern, so the rescue would turn every request into a NotAllowedError, admins included, with no hint as to why.

Pattern Matching Errors and Fixes

Pattern matching raises two families of error. NoMatchingPatternError and its subclass NoMatchingPatternKeyError are runtime mismatches on => or an exhaustive case/in; both sit under StandardError in Ruby’s exception hierarchy, so a plain rescue catches them. The other three messages here are SyntaxErrors that stop the file from loading at all. Every transcript comes from Ruby 3.4.10, which renders hashes as {role: :user} in messages; Ruby 3.3 and earlier print the same errors with the older Hash#inspect format.

5: String === 5 does not return true (NoMatchingPatternError)

Ruby
case 5
in String
  "a string"
end
Shell
$ ruby no_else.rb
no_else.rb:1:in '<main>': 5: String === 5 does not return true (NoMatchingPatternError)

Cause: A => or a case/in with a single branch and no else met a value that matched nothing. The message is the value’s inspect, a colon, and the check that failed. The same error spells a wrong length as [1, 2, 3]: [1, 2, 3] length mismatch (given 3, expected 2), and a mismatched hash value as {role: :user}: :admin === :user does not return true.

With two or more in branches, the message drops the explanation and prints only the value:

Ruby
def classify(value)
  case value
  in Integer then :int
  in String then :str
  end
end
 
classify(:sym)
Shell
$ ruby classify.rb
classify.rb:2:in 'Object#classify': sym (NoMatchingPatternError)
	from classify.rb:8:in '<main>'

Fix: Add an else branch when a non-matching value is a legitimate input, switch to in when you only need a yes or no, or wrap the => in a rescue NoMatchingPatternError as the AdminController does. When the message reads length mismatch, add a splat ([first, *rest]) if the array can be longer than the pattern.

key not found: :email (NoMatchingPatternKeyError)

Ruby
{name: "Ada"} => {email:}
Shell
$ ruby missing_key.rb
missing_key.rb:1:in '<main>': {name: "Ada"}: key not found: :email (NoMatchingPatternKeyError)

Cause: A hash pattern named a key the hash does not have, on a => or a single-branch case/in. NoMatchingPatternKeyError is a subclass of NoMatchingPatternError, so an existing rescue NoMatchingPatternError still catches it, and it carries the missing key and the value that was matched:

Ruby
begin
  {name: "Ada"} => {email:}
rescue NoMatchingPatternKeyError => e
  [e.key, e.matchee]
end
# => [:email, {name: "Ada"}]

A case/in with two or more branches raises the plain NoMatchingPatternError for the same situation, with the hash’s inspect as the whole message.

Fix: Test with in first, or add an else branch. A key holding nil is not a missing key: {email: nil} in {email:} is true and binds email to nil, while {} in {email:} is false:

Ruby
data = {name: "Ada"}
 
contact = if data in {email:} then email else :absent end
contact # => :absent
 
{email: nil} in {email:} # => true
email # => nil
{} in {email:} # => false

duplicated variable name (SyntaxError)

Shell
$ ruby -e 'case [1, 1]; in [x, x]; end'
-e: -e:1: syntax error found (SyntaxError)
> 1 | case [1, 1]; in [x, x]; end
    |                     ^ duplicated variable name

Cause: One pattern binds the same name twice. A name in a pattern always binds, so [x, x] would mean “bind x, then bind x again”, not “two equal elements”. The same message appears for a name bound on both sides of an alternative, in [Integer => x] | [String => x], because either side could be the one that binds it.

Fix: Pin the second occurrence: in [x, ^x] matches [1, 1] and rejects [1, 2]. Inside an alternative, a name that starts with an underscore is allowed, so in [Integer => _x] | [String => _x] compiles and binds _x:

Ruby
case [1, 1]
in [x, ^x]
  "same"
else
  "different"
end
# => "same"
 
case [1, 2]
in [x, ^x]
  "same"
else
  "different"
end
# => "different"
 
case [1]
in [Integer => _x] | [String => _x]
  _x
end
# => 1

expected a label as the key in the hash pattern (SyntaxError)

Shell
$ ruby -e 'case {"a" => 1}; in {"a" => 1}; end'
-e: -e:1: syntax errors found (SyntaxError)
> 1 | case {"a" => 1}; in {"a" => 1}; end
    |                      ^~~ expected a label as the key in the hash pattern
    |                         ^ expected a `}` to close the pattern expression
    |                            ^ expected a delimiter after the patterns of an `in` clause
    |                            ^ expected an identifier after the `=>` operator
    |                              ^ unexpected '}', ignoring it
    |                              ^ unexpected '}', expecting end-of-input

Cause: Hash patterns accept only symbol keys, written as labels (name:, or "first name": for a symbol with a space). A string key with => is not pattern syntax; the first caret is the real diagnosis and the rest are the parser recovering from it. The runtime half of the same trap is silent: a hash with string keys, which is what JSON.parse returns by default, matches no hash pattern at all.

Fix: Parse with symbol keys, or convert them:

Ruby
require "json"
 
JSON.parse('{"name": "Ada"}') in {name:} # => false
JSON.parse('{"name": "Ada"}', symbolize_names: true) in {name:} # => true
{"name" => "Ada"}.transform_keys(&:to_sym) in {name:} # => true

unexpected 'in', ignoring it (SyntaxError)

Shell
$ ruby -e 'case 1; when 1; in Integer; end'
-e: -e:1: syntax error found (SyntaxError)
> 1 | case 1; when 1; in Integer; end
    |                 ^~ unexpected 'in', ignoring it

Cause: A when and an in in the same case. Once the parser has seen when, the case is a case/when, and in has no meaning there.

Fix: Pick one construct. Everything a when can test works as a value pattern after in, since both use ===, so converting the when branches is usually the shorter route; otherwise split the work into two case expressions:

Ruby
w = case 5
    when Integer then :when_ok
    end
i = case 5
    in Integer then :in_ok
    end
[w, i] # => [:when_ok, :in_ok]

What Changed Since Ruby 3.0

Pattern matching left its experimental phase in stages, which is why older posts and answers disagree about what is safe to use. The full grammar, including the undefined-behavior appendix that covers variables bound in a failed branch, lives in the Ruby 3.4 pattern matching reference. The history, as recorded in each release’s NEWS file:

  • Ruby 2.7 introduced pattern matching (case/in) as an experimental feature, with a warning that -W:no-experimental could silence.
  • Ruby 3.0 made case/in stable, redesigned one-line matching around the new => and the standalone in (both experimental), and added the find pattern (experimental).
  • Ruby 3.1 made one-line => and in stable, allowed dropping the brackets around their pattern, let ^ take a parenthesized expression, and let it pin instance, class, and global variables.
  • Ruby 3.2 made the find pattern stable, added Data.define, and gave MatchData, Time, and Date a deconstruct_keys.
  • Ruby 3.3 changed nothing in pattern matching.
  • Ruby 3.4 changed nothing in the syntax, but its new Hash#inspect format changed the text of every NoMatchingPatternError that mentions a hash.

None of the forms in this guide print an experimental warning on Ruby 3.4, with or without -W; the Warning[:experimental] = false advice from the 2.7 era no longer does anything useful.

Pattern matching earns its place wherever code inspects the shape of data before acting on it: dispatching on [:ok, value] tuples, validating params, or walking a parsed JSON document. Reach for case/in when there are several shapes, => when there is one shape you trust, and in when you want a boolean; pin what you mean to compare, and keep the else branch unless a mismatch is a bug.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

What does the pin operator do in Ruby pattern matching?
The pin operator, a caret before a name, tells Ruby to compare against an existing variable instead of binding a new one. Without it, a bare name in a pattern always rebinds. Since Ruby 3.1 you can also pin expressions in parentheses and instance or global variables.
How do I pattern match a hash in Ruby?
Use a hash pattern with symbol keys inside case/in, such as in {name: String => name, role:}. Hash patterns match partially by default, so extra keys are fine; add **nil to require an exact key set. String keys never match, so parse JSON with symbolize_names: true first.
What is the difference between => and in in Ruby pattern matching?
Both match one value against one pattern without a case. The => form raises NoMatchingPatternError when the match fails and returns no value, so it suits unpacking data you trust. The in form returns true or false and never raises, so it suits conditions.
How do I make a custom class pattern-matchable in Ruby?
Define deconstruct_keys(keys) returning a hash to support hash patterns, and deconstruct returning an array to support array patterns. Ruby calls them only when a pattern needs them. Struct and Data classes define both already, so a Data.define value matches with no extra code.
Is pattern matching still experimental in Ruby?
No. case/in has been stable since Ruby 3.0, the standalone => and in forms since 3.1, and the find pattern since 3.2. Ruby 3.4 prints no experimental warning for any of them, so the old Warning[:experimental] = false workaround is unnecessary.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
Pulkit Goyal

Pulkit Goyal

Our guest author Pulkit is a senior full-stack engineer and consultant. In his free time, he writes about his experiences on his blog.

All articles by Pulkit Goyal

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal