Deconstruct and Deconstruct_keys

Published by Programming Line in Ruby. Last updated .

When you write case value in pattern, Ruby doesn’t just peek at instance variables — it asks your object two very specific questions: “if I need you as an array, what are your parts?” and “if I need you as a hash, what are your named fields?” Those questions are answered by two ordinary instance methods you write yourself: deconstruct and deconstruct_keys. Implementing them is what turns any custom class — a Point, a Money, a domain object like Order — into something Ruby’s pattern matching can destructure just as naturally as it destructures arrays and hashes.

Overview: How Pattern Matching Talks to Your Objects

Ruby’s case/in pattern matching (stabilized in Ruby 3.0, refined through 3.2+) supports two families of structural patterns: array patterns like in [x, y] and hash patterns like in {x:, y:}. Arrays and hashes already know how to match these directly, but a plain object — an instance of a class you wrote — has no built-in notion of “my elements” or “my keys.” So Ruby defines a simple protocol: before trying to match an array pattern against a non-Array object, it checks whether the object responds to deconstruct, calls it, and matches the pattern against whatever Array comes back. Before matching a hash pattern against a non-Hash object, it checks for deconstruct_keys, calls it, and matches against the Hash that comes back.

This is exactly the same spirit as to_s for string conversion or each for enumerability: Ruby favors small, well-known protocol methods over baking special cases into the language. Any object that implements these two methods gets full pattern-matching support for free, including nested patterns, guard clauses, and the class-check shorthand ClassName(...). Under the hood, Array itself defines deconstruct as returning self, and Hash defines deconstruct_keys as returning self — which is why array and hash patterns “just work” without you writing anything. Structs generated with Struct.new get both methods generated automatically from their members, which is one reason Struct-based value objects are so pleasant to pattern-match against.

The two methods answer different structural questions and are used by different pattern shapes:

  • deconstruct — called for array patterns: in [a, b], in [a, *rest], find patterns like in [*, target, *], and the positional form of a class pattern, in Point(x, y).
  • deconstruct_keys(keys) — called for hash patterns: in {a:, b:}, in {a:, **rest}, and the keyword form of a class pattern, in Point(x:, y:).

Both a class pattern like Point(x, y) and Point(x:, y:) additionally check is_a?(Point) (more precisely, Point === value) before attempting to deconstruct, so you get a type check and a structural check in one line.

Syntax

class MyClass
  def deconstruct
    [value1, value2]
  end

  def deconstruct_keys(keys)
    { key1: value1, key2: value2 }
  end
end
Part Meaning
deconstruct Takes no arguments. Must return an Array. Powers array patterns and the positional form of class patterns.
deconstruct_keys(keys) Takes exactly one argument. Must return a Hash. Powers hash patterns and the keyword form of class patterns.
keys Either an Array of the Symbol keys the pattern is actually asking for, or nil when the pattern needs every key (for example when it uses **rest without a fixed key list). You’re free to ignore this parameter and always return the full hash — most real-world implementations do — but it lets you optimize expensive lookups when only specific keys were requested.

You only need to implement the method(s) that match the pattern shapes you intend to support. A class that will only ever be matched with hash patterns doesn’t need deconstruct at all, and vice versa — though implementing both is common and cheap.

Examples

Example 1: Array patterns with deconstruct

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct
    [x, y]
  end
end

point = Point.new(3, 4)

case point
in [x, y]
  puts \"Point at x=#{x}, y=#{y}\"
end
Point at x=3, y=4

The pattern [x, y] is an array pattern, so Ruby checks whether point responds to deconstruct. It does, so Ruby calls point.deconstruct, gets back [3, 4], and matches that array against [x, y] — binding x = 3 and y = 4 as local variables inside the in branch, exactly as if you’d written x, y = [3, 4].

Example 2: Hash patterns with deconstruct_keys

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct_keys(keys)
    { x: x, y: y }
  end
end

point = Point.new(10, 20)

case point
in { x:, y: }
  puts \"x is #{x}, y is #{y}\"
end

case point
in { x: 10 }
  puts \"x equals 10, and the rest of the hash is ignored\"
end
x is 10, y is 20
x equals 10, and the rest of the hash is ignored

The shorthand { x:, y: } means “match a hash-like object that has keys :x and :y, and bind their values to local variables of the same name.” Ruby calls point.deconstruct_keys([:x, :y]), gets { x: 10, y: 20 }, and matches. Notice the second case: a hash pattern doesn’t have to mention every key the object has — { x: 10 } matches successfully even though y is present but unmentioned. This is different from array patterns, where [x] would not match a two-element array — array patterns are strict about length unless you use a splat like [x, *].

Example 3: Both together, with class patterns

class Success
  attr_reader :value

  def initialize(value)
    @value = value
  end

  def deconstruct
    [value]
  end

  def deconstruct_keys(keys)
    { value: value }
  end
end

class Failure
  attr_reader :error

  def initialize(error)
    @error = error
  end

  def deconstruct
    [error]
  end

  def deconstruct_keys(keys)
    { error: error }
  end
end

def handle(result)
  case result
  in Success(value)
    \"Succeeded with #{value}\"
  in Failure(error:)
    \"Failed: #{error}\"
  end
end

puts handle(Success.new(42))
puts handle(Failure.new(\"timeout\"))
Succeeded with 42
Failed: timeout

Success(value) is a class pattern with parentheses containing a bare identifier, so Ruby treats it as the positional (array) form: it first checks result.is_a?(Success), then calls deconstruct and matches [value] against the returned [42]. Failure(error:) uses the keyword form instead, because of the trailing colon — Ruby checks result.is_a?(Failure), then calls deconstruct_keys([:error]) and matches against { error: "timeout" }. This pattern — a small hierarchy of result-like objects matched with class patterns — is a common, expressive way to write branching logic in modern Ruby without a pile of if/elsif or manual is_a? checks.

How It Works Step by Step

When Ruby evaluates a case value; in pattern; ...; end expression, it works through the in clauses in order:

  1. Ruby looks at the shape of the current in pattern: is it a literal, an array pattern, a hash pattern, a class pattern, a variable binding, or a combination via | and nesting?
  2. If the pattern is an array pattern and value is not already an Array, Ruby checks value.respond_to?(:deconstruct). If false, this clause simply fails to match and Ruby moves to the next in. If true, Ruby calls value.deconstruct and uses the returned array for matching.
  3. If the pattern is a hash pattern and value is not already a Hash, Ruby checks value.respond_to?(:deconstruct_keys). If false, the clause fails to match. If true, Ruby calls value.deconstruct_keys(keys), where keys is the list of symbol keys the pattern references (or nil if the pattern needs everything), and uses the returned hash.
  4. If the pattern includes a class check (ClassName(...) or a bare ClassName), Ruby first evaluates ClassName === value before attempting deconstruction. If that fails, deconstruction never happens.
  5. Ruby matches each element of the returned array (or each key/value of the returned hash) against the corresponding sub-pattern, recursively applying the same rules for nested patterns, and binds any plain identifiers as local variables as it goes.
  6. If every part matches, the clause’s body runs and the case expression evaluates to whatever that body evaluates to — no further in clauses are tried.
  7. If no in clause matches and there’s no else, Ruby raises NoMatchingPatternError (or the more specific NoMatchingPatternKeyError for a hash pattern that matched the object’s type but was missing a required key).

Two details worth internalizing: deconstruction only happens when it’s needed (an array pattern never triggers deconstruct_keys, and a hash pattern never triggers deconstruct), and it can happen more than once if you write multiple in clauses that each need it — Ruby doesn’t cache the result between clauses, so avoid doing expensive work inside these methods if a value might be matched against many patterns.

Common Mistakes

Mistake 1: Forgetting the keys parameter on deconstruct_keys

Ruby always calls deconstruct_keys with exactly one argument. Defining it with no parameters looks harmless until it’s actually used in a hash pattern:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct_keys
    { x: x, y: y }
  end
end

case Point.new(1, 2)
in { x:, y: }
  puts \"matched\"
end
ArgumentError: wrong number of arguments (given 1, expected 0)

Ruby tries to call deconstruct_keys([:x, :y]), but the method takes zero arguments, so it raises ArgumentError before matching ever happens. The fix is to always declare the parameter, even if you never use it:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct_keys(keys)
    { x: x, y: y }
  end
end

case Point.new(1, 2)
in { x:, y: }
  puts \"matched: x=#{x}, y=#{y}\"
end
matched: x=1, y=2

Mistake 2: Using a hash pattern on an object that only implements deconstruct

Array patterns and hash patterns are not interchangeable — a class that only defines deconstruct cannot be matched with a hash pattern, no matter how sensible that seems:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct
    [x, y]
  end
end

point = Point.new(5, 6)

case point
in { x:, y: }
  puts \"x=#{x}, y=#{y}\"
end
NoMatchingPatternError (Point does not implement deconstruct_keys)

Since Point doesn’t respond to deconstruct_keys, the hash pattern silently fails to match, and because there’s no other in clause and no else, Ruby raises NoMatchingPatternError. The fix is to implement both methods whenever you want a class to support both pattern shapes:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def deconstruct
    [x, y]
  end

  def deconstruct_keys(keys)
    { x: x, y: y }
  end
end

point = Point.new(5, 6)

case point
in { x:, y: }
  puts \"x=#{x}, y=#{y}\"
end
x=5, y=6

A related trap: because array patterns check length strictly, if deconstruct returns a different number of elements than a pattern expects, the match fails too — for example [x, y] will not match a deconstruct result of [3, 4, 5] unless you write [x, y, *] to allow trailing elements.

Best Practices

  • Implement both deconstruct and deconstruct_keys together for value-like objects (points, money, coordinates) so callers can use whichever pattern style reads better in context.
  • Keep both methods pure and cheap — no I/O, no mutation of the receiver — since Ruby may call them once per in clause it tries.
  • Prefer Struct.new(:x, :y, keyword_init: true) for simple value objects; it generates working deconstruct and deconstruct_keys for you, so you only need to hand-write these methods when a plain class gives you more control (validation, computed fields, a richer API).
  • Always accept the keys parameter in deconstruct_keys, even if you ignore it — Ruby’s call signature is fixed and won’t tolerate a zero-arity method.
  • Return a fresh Hash/Array rather than an internal, mutable instance variable, so a caller pattern-matching on your object can’t accidentally end up with a live reference into your object’s private state.
  • Use class patterns (ClassName(...)) instead of a bare array or hash pattern when you also want a type check — it reads clearly and avoids accidentally matching an unrelated object that happens to have the same shape.
  • Reach for case/in and these deconstruction methods when you’re branching on the shape of data (a result type, a parsed token, an event payload); stick with plain case/when or if for simple value or class comparisons.

Practice Exercises

  1. Write a Rectangle class with width and height, implement deconstruct_keys, and use a case/in hash pattern with a guard clause (in {width:, height:} if width == height) to print "It's a square!" versus "It's a rectangle."
  2. Give a Temperature class (storing degrees and a :celsius/:fahrenheit unit symbol) both deconstruct and deconstruct_keys, then match an array of several Temperature instances, printing a converted Fahrenheit value whenever the unit is :celsius. Expected: for a 100-degree Celsius reading, your branch should compute and print 212.0.
  3. Without running it, predict what case [1, 2, 3]; in [Integer => a, *rest]; puts "a=#{a}, rest=#{rest}"; end prints, then verify by running it — this exercises array pattern matching directly on a built-in Array, where deconstruct just returns self.

Summary

  • deconstruct lets a custom object act like an array in case/in array patterns; it takes no arguments and must return an Array.
  • deconstruct_keys(keys) lets a custom object act like a hash in case/in hash patterns; it takes one argument (the requested keys, or nil for all of them) and must return a Hash.
  • Ruby only calls these methods when the matching pattern actually needs them — an array pattern never touches deconstruct_keys, and a hash pattern never touches deconstruct.
  • Class patterns like ClassName(...) combine a type check (is_a?) with deconstruction, giving you type-safe destructuring in one expression.
  • Array, Hash, and generated Struct classes already implement these methods, which is why they pattern-match out of the box.
  • A missing keys parameter or a missing method entirely are the two most common reasons a hash or array pattern silently fails to match, or raises an error.