JSON to Dart: null-safe models for Flutter
Dart's jsonDecode returns Map<String, dynamic>, which gives no type safety and no autocompletion. Flutter apps therefore wrap API responses in model classes with a fromJson factory. This JSON to Dart generator writes those classes from a sample: final fields, a constructor with named parameters marked required, nullable types where values can be missing, and conversion code for nested objects, lists and dates.
Example
{
"id": 42,
"title": "Write docs",
"done": false,
"priority": 2.5,
"tags": ["docs"],
"owner": { "name": "Ada", "email": null },
"due": "2026-10-01T09:00:00Z",
"subtasks": [
{ "title": "Outline", "estimate": 2 },
{ "title": "Draft" }
]
}
Task taskFromJson(String str) => Task.fromJson(json.decode(str));
String taskToJson(Task data) => json.encode(data.toJson());
class Task {
final int id;
final String title;
// ...
final DateTime due;
final List<Subtask> subtasks;
Task({ required this.id, required this.title, /* ... */ });
factory Task.fromJson(Map<String, dynamic> json) => Task(
id: json["id"],
title: json["title"],
priority: json["priority"]?.toDouble(),
due: DateTime.parse(json["due"]),
subtasks: List<Subtask>.from(json["subtasks"].map((x) => Subtask.fromJson(x))),
// ...
);
}
class Subtask {
final String title;
final int? estimate;
Subtask({ required this.title, this.estimate });
}
priority uses ?.toDouble() because JSON numbers like 2 decode as int in Dart even when the field is a double; this conversion avoids a runtime type error. estimate is int? and not required. Dates are parsed with DateTime.parse and written back with toIso8601String().
Using the model in Flutter
final response = await http.get(Uri.parse(url));
final task = taskFromJson(response.body);
Text(task.title);
For a list endpoint, paste an array: the class describes one element and taskFromJson returns a List<Task>.
Handwritten style vs. code generation packages
The output is plain Dart with no dependencies, which is ideal for small apps and for learning how JSON mapping works. Larger Flutter projects often use json_serializable or freezed, which generate the same fromJson/toJson code at build time from annotations. The field list generated here is a quick start for those too: copy the fields, add @JsonSerializable() and run build_runner.
Immutable updates with copyWith
The generated fields are final, which suits Flutter's rebuild model, but you often need a changed copy, for example when a user toggles done. Add a copyWith method by hand:
Task copyWith({bool? done}) => Task(
id: id, title: title, done: done ?? this.done, priority: priority,
tags: tags, owner: owner, due: due, subtasks: subtasks,
);
Packages like freezed generate this automatically if your models grow. Keep the parameter list in sync when you add fields, since the compiler will not remind you about a forgotten property in a hand-written copyWith.
Type mapping
| JSON | Dart |
|---|---|
| integer / decimal | int / double |
| boolean / string | bool / String |
| ISO date | DateTime |
| array | List<T> |
| optional | T?, not required |
| null only | dynamic |
Common pitfalls
- "type 'int' is not a subtype of type 'double'" – a field that is sometimes whole and sometimes decimal. Keep the
toDouble()call or include both kinds of value in your sample. - Null errors at runtime – a field that was always present in the sample but is missing in real responses. Make it nullable and drop
required. - Naming: keys like
user_namebecomeuserNamefields whilefromJsonkeeps readingjson["user_name"].
Format the result with dart format. For XML APIs, use XML to Dart. Check JSON with the JSON Validator or explore it in the JSON Viewer. Generation runs in your browser.