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?
answer
- if (json case ...) one pattern
- map pattern ignores extra keys
- typed variables check the type
- list pattern plus rest ...
- when guard after the match
basics
~20 sWrite 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 linesimport '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
Know the shape of if (json case {'name': String city}) and that the variables exist only inside the branch.
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.
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.
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