In Ruby pattern matching, why does a hash pattern match extra keys while an array pattern must match the whole array, and how do rest patterns change that?
answer
- arrays: whole, hashes: subset
- {} matches only empty
- **nil forbids extra keys
- *rest and **rest bind leftovers
- Symbol keys only: symbolize_names
basics
~20 sAn array pattern must account for every element, so [Integer, Integer] rejects a three-element array unless you add *rest. A hash pattern checks only the keys it names, so extras are ignored; **nil forbids them, **rest collects them, and {} matches only an empty hash.
solid answer
~40 sRuby's reference spells out the asymmetry: arrays match only as a *whole*, while a hash pattern "matches even if there are other keys besides the specified part". So `[Integer, Integer]` fails against `[1, 2, 3]`, and you add `*` or `*rest` to accept more elements. For hashes the default is lenient, which suits JSON payloads that grow new fields; `**nil` makes it strict (no other keys), `**rest` binds the leftovers as a Hash, and `{}` is the single exception that matches only an empty hash. Hash patterns match **Symbol keys only** — a key must be written as a label like `type:` — so a payload from `JSON.parse(body)` with String keys never matches; parse with `symbolize_names: true`. Key-only entries such as `text:` both require the key and bind a local of the same name.
code
ruby · 18 linesrequire "json"
raw = '{"type":"message","text":"hi","ts":1700000000}'
p((JSON.parse(raw) in {type: "message"})) # => false, String keys
event = JSON.parse(raw, symbolize_names: true)
p((event in {type: "message"})) # => true, extra keys ignored
p((event in {type: "message", text: String, **nil})) # => false, ts is extra
case event
in {type: "message", text:, **meta}
p text # => "hi"
p meta # => {ts: 1700000000}
end
p(([1, 2, 3] in [Integer, Integer])) # => false, whole array
p(([1, 2, 3] in [Integer, *])) # => truego deeper
Know that array patterns must match every element and hash patterns ignore extra keys, and that {} matches only an empty hash.
Use *rest, **rest and **nil deliberately, explain key-only binding, and remember that hash patterns match Symbol keys only, which matters for parsed JSON.
Design payload patterns that tolerate producers adding fields, reserve **nil for strict internal formats, and normalise keys at the boundary so matches never fail silently.
Decide how strictly external payloads are validated: lenient patterns for evolving third-party events versus strict schemas where unknown fields signal a contract breach.
## The asymmetry Ruby's pattern-matching reference highlights "an important difference between array and hash pattern behavior": arrays match only a **whole** array, while a hash pattern matches **even if there are other keys**. | Pattern | Subject | Matches? | Why | |---|---|---|---| | `[Integer, Integer]` | `[1, 2, 3]` | no | three elements, pattern accounts for two | | `[Integer, *]` | `[1, 2, 3]` | yes | `*` absorbs the rest | | `[first, *rest]` | `[1, 2, 3]` | yes | `first = 1`, `rest = [2, 3]` | | `{a: Integer}` | `{a: 1, b: 2}` | yes | extra `b:` is ignored | | `{a: Integer, **nil}` | `{a: 1, b: 2}` | no | `**nil` forbids other keys | | `{a: Integer, **rest}` | `{a: 1, b: 2}` | yes | `rest = {b: 2}` | | `{}` | `{a: 1}` | no | `{}` matches only an empty hash | | `{}` | `{}` | yes | the one exact-match hash pattern | The design follows how the data is used: positions in an array carry meaning, so an unexpected extra element usually means a different shape; hashes are records whose producers add fields over time, so ignoring unknown keys is the useful default. ## Why the lenient hash default suits webhooks A chat service that posts `{type: "message", text: "hi", ts: 1700000000}` today may add `thread_ts` and `blocks` next month. A router written as ```ruby case event in {type: "message", text:} handle_message(text) end ``` keeps working when fields are added. If you *do* need to reject unknown fields — say a strict internal command format — write `**nil`. ## Rest patterns bind the leftovers - `*rest` in an array pattern binds an **Array** of the remaining elements (possibly empty). - `**rest` in a hash pattern binds a **Hash** of the keys not named in the pattern. - A bare `*` or `**` accepts leftovers without binding them. ```ruby case event in {type: "message", text:, **meta} store(text, meta) # meta holds every other key end ``` ## Hash keys must be Symbols Hash patterns support **only Symbol keys**; the reference says so directly, and the parser expects a label (`type:` or `"type":`) as each key. Writing `in {"type" => "message"}` is a syntax error. The practical consequence: ```ruby JSON.parse('{"type":"ping"}') # {"type" => "ping"} JSON.parse('{"type":"ping"}') in {type: "ping"} # => false JSON.parse('{"type":"ping"}', symbolize_names: true) in {type: "ping"} # => true ``` The first parse produces String keys, so the pattern looks for `:type`, does not find it and fails — silently with `in`, with an error from `=>` or an exhaustive `case/in`. Parse webhook bodies with `symbolize_names: true` (or transform the keys) before matching. ## Key-only entries bind In a hash pattern, `text:` with no sub-pattern means two things at once: 1. the key `:text` must be **present**; 2. its value is bound to a local named `text`. Adding a sub-pattern keeps the check but stops the automatic binding: `text: String` requires a String and binds nothing, while `text: String => body` checks and binds to `body`. A key whose name is not a valid local variable cannot be used in key-only form. ## Nesting Both kinds nest freely: `{user: {id: String => uid}, attachments: [{kind: "image"}, *]}` checks the user id's type, binds it, and requires the attachments array to start with an image. Every nested array pattern is still whole-array; every nested hash pattern is still subset-of-keys. ## Summary - Arrays: every element must be matched; use `*`/`*rest` for "and more". - Hashes: only named keys are checked; use `**nil` for "and nothing else", `**rest` to capture extras. - `{}` matches only `{}`. - Only Symbol keys; parse JSON with `symbolize_names: true`.
- Why does `JSON.parse(body) in {type: "ping"}` return false for a ping payload?`JSON.parse` returns String keys by default, and hash patterns only match Symbol keys, so the pattern looks for `:type` and does not find it. Parse with `JSON.parse(body, symbolize_names: true)` or convert the keys before matching. The `in` form reports this as `false`; `=>` would raise.
- What is the difference between `in {text:}` and `in {text: String}`?Both require the `:text` key. The key-only form `text:` also binds its value to a local named `text`. With a sub-pattern, `text: String` additionally checks the value with `String === value` but binds nothing; write `text: String => text` to check and bind.
saying these in an interview costs you the question
- A hash pattern fails if the hash has keys the pattern does not mention.
- [Integer, Integer] matches [1, 2, 3] because the first two elements fit.
- The pattern {} matches any hash, since it names no required keys.
- Hash patterns match String keys too, so JSON.parse output works without options.
- **rest binds an Array of the remaining values.