A data model is a class that securely represents the information coming from an API.
In the previous article, we managed to download data from an API. But we ended up with a List<dynamic>.
Working with dynamic is like walking a tightrope without a net. If you write user['nambre'] instead of user['name'], the editor won’t warn you, the compiler won’t warn you, and your App will crash while the user is using it.
To avoid this, in Flutter we transform those generic JSONs into Dart Classes with a proper name and surname. We call this process Serialization and Deserialization.
The Concept
The data flow in a professional App is always the same:
- JSON (String): What arrives through the internet cable.
- Map (Dictionary): Dart converts the text into a key-value structure (
jsonDecode). - Object (Instance): We convert that map into a
UserorProductclass.
This way, in our UI we will use user.name. If we make a typo, VS Code will mark the line in red before compiling. Safety first!
Creating the Data Model
Suppose the API returns this JSON for a user:
{
"id": 1,
"name": "Luis Llamas",
"email": "[email protected]"
}Let’s create a Dart class to represent it. We’ll create a file at /lib/models/user_model.dart.
Properties
We define the fields as final (because data coming from the server should not change by accident).
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email
});
}Constructor factory fromJson
This is where Deserialization happens. We create a named constructor (Factory) that receives the Map and returns an instance of User.
// Receives a Map<String, dynamic> and returns a User
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'], // Note: Ensure keys match the JSON
);
}We use factory by convention because the constructor can decide which instance to return and is not required to create it using a generative initializer. For this example, a regular named constructor could also be used.
Method toJson
If we ever need to send this user back to the server (in a POST), we need the reverse path: Serialization.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}Generating Models from JSON
Writing fromJson by hand for a user with 3 fields is easy. But if you have a response with 50 fields, nested arrays, and objects within objects, doing it by hand is madness and you’ll surely make mistakes.
For this, there are code generators. My favorite to start with is QuickType.
Go to app.quicktype.io.
- Paste your JSON on the left.
- Select “Dart” on the right.
- Copy the generated code and paste it into your file!
It will save you hours of work and prevent typographical errors.
Integrating the Model into the Service
Now let’s go back to our user_service.dart. We will remove all traces of dynamic and use our shiny new User model.
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user_model.dart'; // Import the model
class UserService {
final String _baseUrl = 'jsonplaceholder.typicode.com';
// Notice: Now we return a list of USER, not dynamic
Future<List<User>> getUsers() async {
final url = Uri.https(_baseUrl, '/users');
final response = await http.get(url);
if (response.statusCode == 200) {
// 1. Decode the String into a List of Maps
List<dynamic> listOfMaps = jsonDecode(response.body);
// 2. Map each map to a User object
// Iterate through the list and convert each item using .fromJson
List<User> users = listOfMaps
.map((item) => User.fromJson(item))
.toList();
return users;
} else {
throw Exception('Error loading data');
}
}
}Breaking Down the .map()
The line .map((item) => User.fromJson(item)) is key.
It takes each “raw” element from the list and passes it through the fromJson factory, returning a list of clean and safe objects.
Using the Model in the UI
Finally, in our screen, the change is small but very practical.
// In the ListView.builder
itemBuilder: (context, index) {
// Now 'user' is of type User, not dynamic
final user = users[index];
return ListTile(
title: Text(user.name), // ✅ Autocomplete and no errors
subtitle: Text(user.email),
// Text(user.nambre) // ❌ The editor would mark an ERROR right here
);
}