Communication between your Flutter application and any API usually involves handling JSON data. To make the JSON data easier to use it is recommended to deserialize it into Dart objects. In this post, we will discuss the recommended approach to deserializing both a single JSON object and a list of JSON objects.
Table of contents
JSON Deserialization in Flutter
JSON Deserialization in Flutter is turning JSON data into Dart objects. Doing this will make it much easier to work with. Luckily in Flutter, this can be done very easily using some packages maintained by Google.
Installing the Packages
To simplify the deserialization process in Flutter, we want to use the Json serializable package, along with its dependencies Json annotation, and Build runner. They can be installed by executing the following command inside your project:
flutter pub add json_annotation && flutter pub add --dev build_runner json_serializable
After executing the command, check your pubspec.yaml
file for the added dependencies. You should see the Json serializable, Json annotation, and Build runner packages included in the dependencies
and dev_dependencies
, like this:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
If you want to follow this tutorial you will also need the Http package because we want to communicate with an API.
flutter pub add http
Therefore your pubspec.yaml
file will end up looking like this:
dependencies:
http: ^1.2.1
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
With the packages installed, we can start creating our models.
Create the Model Classes
In this tutorial, we will be communicating with the SpaceX API. We will be requesting the latest launch and a list of all the launches. Underneath you will see a small fraction of the JSON response we will receive from the API calls.
{
"crew": [
{
"crew": "62dd7196202306255024d13c",
"role": "Commander"
}
],
"date_utc": "2022-10-05T16:00:00.000Z",
"flight_number": 187,
"id": "62dd70d5202306255024d139",
"launch_library_id": "f33d5ece-e825-4cd8-809f-1d4c72a2e0d3",
"launchpad": "5e9e4502f509094188566f88",
"name": "Crew-5",
"rocket": "5e9d0d95eda69973a809d1ec",
"success": true
}
During this tutorial, we will only be focusing on this fraction of the data to make it easy to follow.
From the JSON response, we can determine that we need 2 models: Launch
and Crew
. The Launch
model will have all the above JSON keys and will also have an array of JSON crew objects. Therefore we also need to create a Crew
model. Because the Launch
model will be needing the Crew
model let us create this one first.
Create the Crew Class
To create a model that can be used to deserialize JSON in Flutter. We can create a regular Dart class and add the @JsonSerializable
annotation to it.
import 'package:json_annotation/json_annotation.dart';
part 'crew.g.dart';
@JsonSerializable()
class Crew {
const Crew({
required this.crew,
required this.role,
});
factory Crew.fromJson(Map<String, dynamic> json) =>
_$CrewFromJson(json);
Map<String, dynamic> toJson() => _$CrewToJson(this);
final String crew;
final String role;
}
In the above code, we have created the Crew
class with the crew
and role
attribute to represent the JSON object. As you can see we have a Crew.fromJson
factory that will create a Crew instance from JSON. We also have the toJson
function that can turn an instance of Crew
into JSON.
Create the Launch Class
For the Launch
class we can do the same as we have done for the Crew
class. The only difference are the attributes we will use.
import 'package:json_annotation/json_annotation.dart';
import 'package:json_serialization/crew.dart';
part 'launch.g.dart';
@JsonSerializable()
class Launch {
const Launch({
required this.crew,
required this.dateUtc,
required this.flightNumber,
required this.id,
required this.launchLibraryId,
required this.launchpad,
required this.name,
required this.rocket,
required this.success,
});
factory Launch.fromJson(Map<String, dynamic> json) => _$LaunchFromJson(json);
Map<String, dynamic> toJson() => _$LaunchToJson(this);
final List<Crew> crew;
final DateTime dateUtc;
final int flightNumber;
final String id;
final String? launchLibraryId;
final String launchpad;
final String name;
final String rocket;
final bool? success;
}
In the above code, we added all the attributes that can be found in the JSON data to our Launch
model. The Launch
model also has a crew
attribute that we use to store a list of our Crew
models. Because the Crew
model also has a fromJson
factory we can use the model here.
Generate the Serialization Functions
Now that we have finished creating the models you probably noticed that your IDE marks the _$launchFromJson
and _$LaunchToJson
functions as invalid. This is because those function do not exist yet and they need to be generated. In both models we have part
directive that refers to a .g.dart
file. These are the files we need to generate and we can do this by executing the following command:
dart run build_runner build --delete-conflicting-outputs
The --delete-conflicting-ouputs
flag is not necessarily needed but it ensures that generated files are overwritten.
Matching the Attribute Names with JSON Keys
Even though we generated the .g.dart
files we will still not be able to deserialize JSON response into our models. If we check the launch.g.dart
file, we will see that the JSON keys in both the _$LaunchFromJson
and _$LaunchToJson
functions do not match the JSON keys from the JSON data we receive.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'launch.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Launch _$LaunchFromJson(Map<String, dynamic> json) => Launch(
crew: (json['crew'] as List<dynamic>)
.map((e) => Crew.fromJson(e as Map<String, dynamic>))
.toList(),
dateUtc: DateTime.parse(json['dateUtc'] as String),
flightNumber: (json['flightNumber'] as num).toInt(),
id: json['id'] as String,
launchLibraryId: json['launchLibraryId'] as String?,
launchpad: json['launchpad'] as String,
name: json['name'] as String,
rocket: json['rocket'] as String,
success: json['success'] as bool?,
);
Map<String, dynamic> _$LaunchToJson(Launch instance) => <String, dynamic>{
'crew': instance.crew,
'dateUtc': instance.dateUtc.toIso8601String(),
'flightNumber': instance.flightNumber,
'id': instance.id,
'launchLibraryId': instance.launchLibraryId,
'launchpad': instance.launchpad,
'name': instance.name,
'rocket': instance.rocket,
'success': instance.success,
};
We can solve this in 3 ways, however, I do not recommend the last approach.
1. build.yaml with Field Rename
We can add a build.yaml
file in the root of our project to make sure that all the JSON keys are renamed to Snake case.
targets:
$default:
builders:
json_serializable:
options:
field_rename: snake
This will be the best solution for this API because all the keys follow the same convention.
However, some APIs might have different conventions for their keys. If that is the case you can make use of the @JsonKey
annotation.
2. Use the @JsonKey Annotation
The @JsonKey
annotation is very convenient to rename individual keys. As mentioned before it is the better solution in case the JSON data from the API does not follow a convention.
import 'package:json_annotation/json_annotation.dart';
import 'package:json_serialization/crew.dart';
part 'launch.g.dart';
@JsonSerializable()
class Launch {
const Launch({
required this.crew,
required this.dateUtc,
required this.flightNumber,
required this.id,
required this.launchLibraryId,
required this.launchpad,
required this.name,
required this.rocket,
required this.success,
});
factory Launch.fromJson(Map<String, dynamic> json) => _$LaunchFromJson(json);
Map<String, dynamic> toJson() => _$LaunchToJson(this);
final List<Crew> crew;
@JsonKey(name: 'date_utc')
final DateTime dateUtc;
@JsonKey(name: 'flight_number')
final int flightNumber;
final String id;
@JsonKey(name: 'launch_library_id')
final String launchLibraryId;
final String launchpad;
final String name;
final String rocket;
final bool success;
}
As you can see we added 3 @JsonKey
annotations to rename the keys that were not generated correctly.
3. Rename the attribute names of the Launch model
The last approach is to rename the attribute names themselves to match the JSON keys. However, you will no longer follow the Lower Camel case convention recommended by Dart.
Regenerating the .g.dart Files
After having chosen one of the options you can execute the command again:
dart run build_runner build --delete-conflicting-outputs
Create the Launch Client
To test if our models are working correctly we can create the following LaunchClient
class that will communicate with the SpaceX API. In this client, we will make both the API call to get a single JSON object and a list of JSON objects.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:json_serialization/launch.dart';
class LaunchClient {
LaunchClient({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();
final http.Client _httpClient;
Future<Launch> getLaunch() async {
final request = Uri.parse('https://api.spacexdata.com/v5/launches/latest');
final response = await _httpClient.get(request);
final bodyJson = jsonDecode(response.body) as Map<String, dynamic>;
return Launch.fromJson(bodyJson);
}
Future<List<Launch>> getLaunches() async {
final request = Uri.parse('https://api.spacexdata.com/v5/launches');
final response = await _httpClient.get(request);
final bodyJson = jsonDecode(response.body) as List;
return bodyJson
.map((json) => Launch.fromJson(json as Map<String, dynamic>))
.toList();
}
}
In the above code, we created 2 functions that both return a Future
. The getLaunch
function returns a single JSON object and we deserialize it into an instance of the Launch
model. Before we use the Launch.fromJson
factory we first have to decode the response. We do this by using the JsonDecode
function. Make sure to cast it as Map<String, dynamic>
or else our factory will not accept it.
The getLaunches
function returns a list of Launch
models. We also decode the response here using the JsonDecode
function, however here we cast it to a List
. Afterward, we map over this list to convert every item into a Launch
instance.
Deserialize a JSON Object in Flutter
After finishing the LaunchClient
we can now use it to deserialize JSON into our Dart models.
import 'package:json_serialization/launch_client.dart';
Future<void> main() async {
final launch = await LaunchClient().getLaunch();
print(launch.crew);
print(launch.dateUtc);
print(launch.flightNumber);
print(launch.id);
print(launch.launchLibraryId);
print(launch.launchpad);
print(launch.name);
print(launch.rocket);
print(launch.success);
}
In the above example, we call the getLaunch
function and save its value on the launch
variable. Afterward, we print all the attributes to demonstrate that it is working as intended.
It might be possible that you run into errors when trying to run the function this could be because of
null
values. In that case you have to make sure that the mentioned attribute is nullable like we have done for thelaunchLibraryId
andsuccess
attributes.
Deserialize a List of JSON objects in Flutter
To showcase that we are also able to deserialize a list of JSON objects we are calling the getLaunches
function from the LaunchClient
class.
import 'package:json_serialization/launch_client.dart';
Future<void> main() async {
final launches = await LaunchClient().getLaunches();
final launch = launches.first;
print(launch.crew);
print(launch.dateUtc);
print(launch.flightNumber);
print(launch.id);
print(launch.launchLibraryId);
print(launch.launchpad);
print(launch.name);
print(launch.rocket);
print(launch.success);
}
As you can see we are only printing the first Launch
instance of the list. However, this is enough to demonstrate that the above code is working.
Conclusion
Deserializing JSON in Flutter can be complicated at first, however I hope this post cleared up the confusion and simplified the process. You know how to create Dart models that can be used to deserialize JSON in Flutter and you also know how to nest multiple models. We also discussed how to handle different JSON key conventions. Other than that you are also aware of how to deserialize a single object and a list of objects.