Course outline

Ruby / Contracts: callables, evaluation, failures, and value models / Lesson 12

Match structured data without overclaiming validation

Use case/in, type and hash patterns, guards, pins, and explicit mismatch policy under the selected Ruby 3.3 baseline.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish a structural pattern match from full domain validation and authorization.
  • Explain binding versus pinning and conditional versus mandatory matching.

A pattern checks only the conditions it expresses

Ruby pattern matching can inspect structured values and bind matching parts. case/in is different from case/when and cannot mix their branch forms in one case expression. Type patterns use their matching contract, and a guard can impose a further condition after a shape matches. A Hash pattern using id: expects symbol-keyed structure; an external JSON object with String keys is not automatically equivalent. Translate external data deliberately rather than blindly changing key representation everywhere. A pattern can improve clarity while still leaving blank text, forbidden identifiers, and out-of-range values to separate validation.

Refine a matched shape with explicit policy

The example binds a String identifier and Integer count, then requires positivity. Extra hash keys are allowed by this pattern, so the extra field does not cause rejection. Use the appropriate closed-shape pattern, including **nil where applicable, if unknown keys must be rejected. The source does not validate the identifierโ€™s format or ownership. Ruby 3.3 syntax is the selected baseline, not a claim about every earlier interpreter. The displayed success is a reasoned local outcome only; no Ruby pattern engine or external-data integration was executed.

ruby A guarded hash pattern that permits extra keys
value = {id: 'A-9', copies: 2, extra: 'ignored'}
case value
in {id: String => id, copies: Integer => copies} if copies > 0
  puts "#{id}:#{copies}"
else
  puts 'invalid shape or count'
end
Expected output
A-9:2

Expected output is A-9:2. The guard checks positivity and the pattern checks the stated types; extra fields are intentionally not rejected. The script was not run.

Pin an existing value instead of rebinding it

A plain variable position in a pattern binds matched data; it does not automatically compare with an existing local of the same name. The pin operator ^ asks the match to use an existing value. This distinction is important when selecting a particular archive record rather than accepting and overwriting any identifier. Keep variables used only after successful matching inside the successful branch. Do not depend on partial bindings left by a failed complex pattern, since such reliance can make control flow fragile and obscure which evidence established a value.

State which identifier the comparison preserves

The array pattern below requires two elements, compares the first with expected through a pin, and captures an Integer in the second. The else branch handles a nonmatching record without turning it into an exception. This is a filtering policy, not full import validation: a product requiring one explicit outcome per record should retain a rejection reason. A rightward assignment using => or a case/in without a suitable else can instead raise NoMatchingPatternError on mismatch. Choose conditional handling or mandatory matching according to whether mismatch is expected or violates a trusted internal invariant.

ruby A pinned identifier and captured quantity
expected = 'A-7'
[['A-7', 2], ['B-8', 3]].each do |record|
  case record
  in [^expected, Integer => copies]
    puts "match:#{copies}"
  else
    puts 'skip'
  end
end
Expected output
match:2
skip

Expected output is match:2 then skip. expected remains the selected comparison value. No interpreter or import pipeline was executed.

Custom deconstruction is executable behavior

Objects can support deconstruct and deconstruct_keys to participate in patterns. Those methods can perform work or expose mutable references, so pattern syntax is not a guarantee of purity, bounded cost, or safe ownership. Keep deconstruction predictable and avoid side effects that depend on a particular number of matching attempts. Validate data at the boundary before granting real authority. In an isolated compatible project, check wrong shapes, wrong key types, extra keys, failed guards, pins, and declared mismatch outcomes. This course reports source reasoning only and no runtime pattern-match evidence.

Common errors and fixes

A supposed comparison silently binds a new identifier.

Why it happens
A variable pattern was used without pinning the existing value.
How to fix it
Use ^ for the intended existing-value comparison.
Prevent it next time
Check binding and comparison roles explicitly in every pattern.

An external String-keyed hash fails a symbol-keyed pattern.

Why it happens
The structural key contract changed across decoding.
How to fix it
Validate and translate the external schema before internal matching.
Prevent it next time
Include the actual decoded key types in fixtures.

An ordinary invalid input raises NoMatchingPatternError unexpectedly.

Why it happens
Mandatory matching was used where mismatch was a normal outcome.
How to fix it
Provide an explicit conditional rejection path.
Prevent it next time
Separate untrusted-input validation from trusted internal destructuring.

Check your understanding

1. Does matching {id: String, copies: Integer} prove a valid archive command?

Show worked solution

No. It checks only the stated shape and matching classes. The identifier can be blank, nonexistent, or unauthorized, and the integer can violate a range or state rule. Extra fields may also remain accepted unless the pattern closes the shape. Keep decoding, structural matching, domain validation, and authorization as distinct evidence boundaries.

2. When is raising on a pattern mismatch appropriate?

Show worked solution

When the caller-facing contract establishes a trusted shape and mismatch indicates a violated programming invariant. For ordinary external variability, an explicit invalid result is usually clearer. Document the chosen behavior and test both matching and nonmatching inputs. The brevity of rightward assignment is not a reason to convert routine rejection into an unexpected crash.

Key takeaways

  • Patterns expose particular structural checks, not complete trust.
  • Binding, pinning, guards, and mismatch policy each carry meaning.

Official references