Pattern Matching in Ruby

· 5 min read · Updated March 17, 2026 · intermediate
ruby ruby-3 pattern-matching

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.