Ruby argument validation with pattern matching
2025-12-12I always forget how to apply the syntax for pattern matching in real-world scenarios, so I’m writing an article to remind myself (and hopefully teach other people, if my SEO is good enough!)
Ruby 2.7 added pattern matching, and future versions have continued to expand on it. I’m assuming a modern Ruby at the time of writing (3.4) for all of these examples.
Two Operators
For argument validation, there are two operators to be aware of:
=>will return nil on success, or throwNoMatchingPatternErrorif the pattern doesn’t match. This is terse, but you have limited flexibility over the exception.inwill return a boolean based on whether the pattern matched. You can then throwArgumentErroror similar yourself.
Both operators sit between the value on the left and the pattern on the right:
3 in Integer # true
3 => Integer # nil
"foo" in Integer # false
"foo" => Integer # NoMatchingPatternError
Scenario
Suppose we have a Point class, here defined with Data: 1
Point = Data.define(:x, :y)
If the fields of Point always needed to have some particular type, we could override the constructor generated by Data to check the type there.
For the sake of example, we’ll avoid doing this, and instead pretend we’re implementing some hypothetical method foobar where only integer components are allowed.
Without pattern matching, our validation might look like this:
def foobar(point)
unless point.is_a?(Point)
raise ArgumentError, "point must be a Point"
end
unless point.x.is_a?(Integer) && point.y.is_a?(Integer)
raise ArgumentError, "point components must be Integers"
end
# ... use `point.x` and `point.y` ...
end
How could this be written to make use of pattern matching instead?
As Short As Possible
The shortest reasonable implementation of this validation uses => to throw an exception if the pattern doesn’t match:
def foobar(point)
point => Point(x: Integer, y: Integer)
# ... use `point.x` and `point.y` ...
end
To break this down - this pattern expects point to be an object of type Point, and that object should have two keys called x and y, which are both of type Integer.
The errors when this fails are… actually not too bad:
foobar("not a point")
# => NoMatchingPatternError:
# Point === "not a point" does not return true
foobar(Point.new(x: 1.2, y: 3.4))
# => NoMatchingPatternError:
# #<data Point x=1.2, y=3.4>:
# Integer === 1.2 does not return true
They aren’t described quite as high-level as I’d like, and they’re also not ArgumentError or TypeError, but they certainly do the job.
You also have the option to unpack point into its fields, if that’s useful for the rest of your code:
def foobar(point)
# vvvv vvvv
point => Point(x: Integer => x, y: Integer => y)
# ... use `x` and `y` ...
end
While we’re on that topic, if you didn’t need to check the types of the fields, you can use Ruby 3.1’s new shorthand hash syntax to do this unpacking even more concisely:
def foobar(point)
point => Point(x:, y:)
# ... use `x` and `y` ...
end
Custom Errors
If you want control over the error which gets raised, use in and raise an exception yourself:
def foobar(point)
unless point in Point(x: Integer, y: Integer)
raise ArgumentError, "point must be a Point with Integer components"
end
end
To raise two distinct errors for the validation stages, like in the original is_a? example, you need to match on two separate patterns - which somewhat diminishes the benefits of pattern matching:
def foobar(point)
unless point in Point
raise ArgumentError, "point must be a Point"
end
# We don't need to check `Point` again.
# Pattern matching will call `Point#deconstruct_keys`,
# which was defined by `Data` to return a Hash of this shape
unless point in { x: Integer, y: Integer }
raise ArgumentError, "point components must be Integers"
end
end
You can shorten this a little bit by using either the unless suffix, or the low-precedence short-circuiting or operator 2, depending on which way around you’d like the pattern and the error. The pattern of X in Y or Z reads quite naturally in my opinion, but I can understand why some developers might not like the keyword soup on display here:
def foobar(point)
# Option 1: unless suffix (error then pattern)
raise ArgumentError, "point must be a Point" unless point in Point
# Option 2: or operator (pattern then error)
point in { x: Integer, y: Integer } or raise ArgumentError, "point components must be Integers"
end
Is this worth the extra verbosity over just letting => throw its own exception? That’s up to you, and your use-case!
Further Thoughts
I’ve already linked it, but the Ruby documentation on pattern matching is a useful and comprehensive reference.
For a guided introduction to some more advanced features of pattern matching which you might be able to apply here, I really enjoyed this talk by Brandon Weaver which walks through using pattern matching to match poker hands.
I’ve mostly thought about type checking in this article, but you could also apply other kinds of patterns to validate input ranges. Hopefully those two resources give some inspiration!
-
If your class isn’t generated by
Dataor similar, you need to implement#deconstruct_keysfor any of these examples to work. ↩ -
I prefer the
oroperator as opposed to||in these situations. I tend to stick to||for strictly boolean-OR operations, where I use the result as a boolean, and useorwhere I only care about the short-circuiting control-flow. The distinction is useful when scanning over code. ↩