skip to content

Pattern Kinds

Patterns test a value's shape and bind its parts: list, map, record and object patterns plus relational and logical checks, in declarations, for-in loops and if-case. Interviewers probe refutability.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart 3, how would you validate and destructure a decoded weather-API JSON map with if-case, and what does each part of the pattern check?

level: middleimportance: must knowfreq 55%

answer

  1. if (json case ...) one pattern
  2. map pattern ignores extra keys
  3. typed variables check the type
  4. list pattern plus rest ...
  5. when guard after the match

basics

~20 s

Write if (json case {'name': String city, 'main': {'temp': num temp}}). It checks that json is a map with those keys and value types, binds city and temp only on a full match, and otherwise runs the else branch.

solid answer

~50 s

`if (json case pattern)` tests one **refutable** pattern and runs the branch only on a full match, with the pattern's variables in scope. A **map pattern** such as `{'name': String city, 'main': {'temp': num temp}}` checks that the value is a `Map`, that each listed key is present, and recurses into the values; it ignores keys you do not list, so the API can add fields. **Typed variable patterns** such as `String city` match only a value of that type. A **list pattern** such as `'weather': [{'description': String summary}, ...]` checks a list whose first element has that shape, with `...` allowing any number of further elements. A `when` guard adds a condition after matching, for example `when humidity <= 100`. If any check fails, nothing is bound and control goes to `else`, without an exception.

code

dart · 28 lines
dart
import 'dart:convert';

class Forecast {
  Forecast(this.city, this.temp, this.humidity, this.summary);
  final String city;
  final double temp;
  final int humidity;
  final String summary;
}

Forecast? parseForecast(Object? json) {
  if (json case {
    'name': String city,
    'main': {'temp': num temp, 'humidity': int humidity},
    'weather': [{'description': String summary}, ...],
  } when humidity >= 0 && humidity <= 100) {
    return Forecast(city, temp.toDouble(), humidity, summary);
  }
  return null;
}

void main() {
  const body = '{"name": "Oslo", "main": {"temp": 3, "humidity": 81}, '
      '"weather": [{"description": "light rain"}], "timezone": 3600}';
  final forecast = parseForecast(jsonDecode(body));
  print('${forecast?.city} ${forecast?.summary}'); // Oslo light rain
  print(parseForecast({'name': 'Oslo'})); // null: 'main' is missing
}

go deeper

for a junior

Know the shape of if (json case {'name': String city}) and that the variables exist only inside the branch.

for a middle

Explain every check the pattern performs: map type, key presence, typed values, nested patterns, list length with rest, and the when guard evaluated after the match.

for a senior

Design the boundary: num versus double for JSON numbers, which fields must be present, how to log a rejected payload, and when to hand larger schemas to generated models.

for a principal

Decide where pattern-based parsing is the team's standard and where generated serialization owns the contract, keeping validation at one clear edge of the app.

## The problem: untrusted JSON `jsonDecode` returns `dynamic`. A weather response might look like this: ```json {"name": "Oslo", "main": {"temp": 3.5, "humidity": 81}, "weather": [{"description": "light rain"}], "timezone": 3600} ``` Without patterns, reading it safely means a ladder of `is` checks, `containsKey` calls and casts. With Dart 3's **if-case**, one pattern states the expected shape, checks it and binds the parts. ## One pattern, many checks ```dart Forecast? parseForecast(Object? json) { if (json case { 'name': String city, 'main': {'temp': num temp, 'humidity': int humidity}, 'weather': [{'description': String summary}, ...], } when humidity >= 0 && humidity <= 100) { return Forecast(city, temp.toDouble(), humidity, summary); } return null; } ``` Read it from the outside in: 1. **Map pattern** `{...}` — `json` must be a `Map`. That also rules out `null`. 2. **Keys** `'name'`, `'main'`, `'weather'` — each must be present. A missing key makes the match **fail**, not throw. Keys the pattern does not mention, such as `'timezone'`, are **ignored**. 3. **Typed variable pattern** `String city` — the value must be a `String`; if so it is bound to `city` with static type `String`. 4. **Nested map pattern** under `'main'` — the value must itself be a map with `temp` and `humidity` of the stated types. 5. **List pattern** `[{'description': String summary}, ...]` — the value must be a `List` whose first element is a map with a `String` description; the **rest element** `...` accepts any number of further elements. Without `...`, a list pattern matches only a list of exactly that length. 6. **Guard** `when ...` — evaluated only **after** the pattern matched, with the bound variables in scope. If it is false, the `if` behaves as a non-match. If every check passes, the then-branch runs with `city`, `temp`, `humidity` and `summary` in scope and fully typed. If any check fails, **nothing** is bound, no exception is thrown, and control reaches the `else` branch or the next statement. ## Choosing the types | Subpattern | Matches | Watch out for | |---|---|---| | `num temp` | `3` and `3.5` | call `toDouble()` when the model wants `double` | | `double temp` | `3.5` | on the Dart VM an integral JSON number such as `3` decodes to an `int`, so the match fails | | `int humidity` | `81` | on the VM a payload value written `81.0` decodes to a `double` and fails the match | | `String? note` | a string or `null` | the key must still be present | ## Where if-case fits - **One shape to test** → `if-case`. Several alternative shapes → a `switch`, which is its own topic. - **Collection literals** accept the same form: in a Flutter `Column`'s `children`, `if (json case {'alert': String a}) Text(a)` adds the widget only on a match. - The pattern variables are **scoped to the branch**; declare a result variable outside if you need the values later. ## Handling the else branch A failed match tells you only that *something* was off, so decide up front what the app does next: - **Return `null` or an empty state** when the screen can render without the data, as `parseForecast` does. - **Log the raw payload** in the else branch when the response should always be valid; a shape change on the server then shows up in logs instead of as a blank widget. - **Fall back to a second pattern** for an older payload version, or use a logical-or pattern that accepts both shapes. - **Throw a `FormatException`** yourself when an invalid payload is a hard error for the caller. ## What the pattern does not do - It does not **convert** values: a string `"3.5"` does not match `num temp`. - It does not report **which** check failed. For user-facing error messages, validate in steps or log the payload in the else branch. - It does not replace generated models for large schemas; code-generated `fromJson` classes are the usual tool there. If-case shines for small payloads, optional sections and defensive checks at the edge.

  • Why does the pattern use `num temp` instead of `double temp`?
    JSON has one number type, and on the Dart VM `jsonDecode` produces an `int` for an integral value such as `3`. A `double temp` subpattern would then fail to match and the whole forecast would be rejected. `num` accepts both, and `temp.toDouble()` gives the model its `double`.
  • What happens when the `when` guard is false in an if-case?
    The pattern has matched, but the if-case as a whole is treated as a non-match: the then-branch does not run and control goes to the `else` branch or the next statement. The variables bound by the pattern are not available there.
  • Would `final {'main': {'temp': num temp}} = json;` be a good replacement?
    No. A declaration cannot fall back to an else branch, so a mismatch throws instead of failing: a missing key in a map pattern throws a `StateError`. Declarations suit values whose shape is already guaranteed; untrusted JSON belongs in an if-case or switch.

saying these in an interview costs you the question

  • Thinks a map pattern fails when the JSON has extra keys
  • Believes a failed if-case match throws an exception
  • Expects "3.5" as a string to match num temp
  • Uses double temp for JSON numbers that may be integral
  • Assumes a list pattern without ... matches longer lists
  • Reads pattern variables after the if-case block
open as a page

In Dart 3, how do you pull the fields of a returned record or an object's getters into local variables with a pattern?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Write a pattern variable declaration: var (city, temp) = latestReading(); for a positional record, final (:city, :temp) = ... for named fields, and var Forecast(:high, :low) = forecast; to read an object's getters into new locals.

open as a page

In Dart, what is the difference between refutable and irrefutable pattern contexts, and why does a bare `case limit:` not bind a new variable?

level: middleimportance: should knowfreq 36%

basics

~20 s

Declarations and assignments are irrefutable: their patterns must match or throw. if-case and switch cases are refutable: a mismatch just fails. In a case, a bare identifier is a constant pattern, so case limit: compares instead of binding.

open as a page

In Dart 3, how do relational patterns like `>= 0 && < 15` and logical-or patterns work, and what rules limit their operands and bindings?

level: middleimportance: should knowfreq 30%

basics

~20 s

A relational pattern such as < 0 compares the value with a constant; && needs both sides to match, || needs one. Operands must be constants, || branches must bind the same variables, and && sides must not share a name.

open as a page

In Dart patterns, when would you use `var t?`, `var t!` or `t as double` on a reading whose temperature may be null, and what happens on a bad value?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Use the null-check pattern var t? when null is expected: null fails the match. Use null-assert var t! when null is a bug: it throws. A cast pattern t as double insists on a type and throws on a wrong one.

open as a page