Aaron Christiansen

Ruby argument validation with pattern matching

2025-12-12

I 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 throw NoMatchingPatternError if the pattern doesn’t match. This is terse, but you have limited flexibility over the exception.
  • in will return a boolean based on whether the pattern matched. You can then throw ArgumentError or 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!

  1. If your class isn’t generated by Data or similar, you need to implement #deconstruct_keys for any of these examples to work. 

  2. I prefer the or operator 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 use or where I only care about the short-circuiting control-flow. The distinction is useful when scanning over code.