How to Deserialize JSON Responses in Flutter

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.

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 the launchLibraryId and success 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.

Tijn van den Eijnde
Tijn van den Eijnde
Articles: 38

Leave a Reply

Your email address will not be published. Required fields are marked *