← → or space · progress saves for Continue on the roadmap
Goal
Catch FormatException, validate top-level shape, and return errors instead of crashing.
Step 1 - Parse string safely
import 'dart:convert';
typedef JsonMap = Map<String, dynamic>;
Result<JsonMap> decodeObject(String raw) {
try {
final v = jsonDecode(raw);
if (v is JsonMap) return Result.ok(v);
return const Result.err('root must be object');
} on FormatException catch (e) {
return Result.err('invalid json: ${e.message}');
}
}
class Result<T> {
final T? value;
final String? error;
const Result.ok(this.value) : error = null;
const Result.err(this.error) : value = null;
bool get isOk => error == null;
}Step 2 - Expect an array root
import 'dart:convert';
Result<List<dynamic>> decodeArray(String raw) {
try {
final v = jsonDecode(raw);
if (v is List<dynamic>) return Result.ok(v);
return const Result.err('root must be array');
} on FormatException catch (e) {
return Result.err('invalid json: ${e.message}');
}
}Step 3 - Todo list payload
Reuse Result<T> from step 1.
import 'dart:convert';
Result<List<Map<String, dynamic>>> decodeTodoMaps(String raw) {
try {
final v = jsonDecode(raw);
if (v is! List<dynamic>) {
return const Result.err('root must be array');
}
final out = <Map<String, dynamic>>[];
for (final item in v) {
if (item is Map<String, dynamic>) {
out.add(item);
} else {
return const Result.err('each todo must be object');
}
}
return Result.ok(out);
} on FormatException catch (e) {
return Result.err('invalid json: ${e.message}');
}
}Practice tasks
- Map
decodeTodoMapsintoResult<List<Todo>>usingTodo.fromJsoninside a loop; fail fast on first bad row with an index in the message. - Accept both
{"todos":[...]}and a raw array[...]with a small dispatcher. - Log the offending substring (first 80 chars) when
FormatExceptionfires (no full secrets in logs).