Parsing JSON in Flutter

Learn about getting and parsing JSON data from the internet when building a cross-platform app using Flutter. By Sardor Islomov.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Using JSON Annotations

Now, you’ll use the json_annotation library to parse the JSON data into objects of your model classes.

Go to the top of cats.dart, and add the following imports to the top:

import 'package:json_annotation/json_annotation.dart';
part 'cats.g.dart';

The part statement imports a file and allows you to use its private variables. You’ll see an error on this statement for now until you later use build_runner to generate the file cats.g.dart.

Next, you need to add the @JsonSerializable() annotation to each class in cats.dart. For example, your Breed class should look like this when you add the annotation:

@JsonSerializable()
class Breed {
  String id;
  String name;
  String description;
  String temperament;

  Breed({
    required this.id,
    required this.name,
    required this.description,
    required this.temperament
  });
}

Make sure you add the annotation before every class in cats.dart.

JSON Conversion Methods

In the next step, you’ll add some factory methods to each class. The build runner plugin will use these methods to create a Dart file to do all the hard work of parsing the JSON data for you.

In the Breed class, add the following after the constructor:

factory Breed.fromJson(Map<String, dynamic> json) => _$BreedFromJson(json);

Map<String, dynamic> toJson() => _$BreedToJson(this);

Each class will include fromJson and toJson. These methods call the generated code that parses the JSON. At this point, you’ll notice some more errors in Android Studio. Don’t worry about these at the moment; you’ll clear them up later.

In BreedList, add the following after the constructor:

factory BreedList.fromJson(final dynamic json) {
  return BreedList(
      breeds: (json as List<dynamic>)
          .map((dynamic e) => Breed.fromJson(e as Map<String, dynamic>))
          .toList());
}

This is the fromJson method you need to parse the JSON array to a list of breeds.

Add fromJson and toJson after the constructor in Cat:

factory Cat.fromJson(Map<String, dynamic> json) => _$CatFromJson(json);

Map<String, dynamic> toJson() => _$CatToJson(this);

Next, after the constructor in CatBreed, add:

factory CatBreed.fromJson(Map<String, dynamic> json) =>
    _$CatBreedFromJson(json);

Map<String, dynamic> toJson() => _$CatBreedToJson(this);

Finally, add the following after the constructor in CatList:

factory CatList.fromJson(dynamic json) {
  return CatList(
      breeds: (json as List<dynamic>)
          .map((dynamic e) => CatBreed.fromJson(e as Map<String, dynamic>))
          .toList());
}

You’ve now added all the fromJson and toJson methods you need in your model classes.

Using build_runner

Your next step is to run the tool that generates the files that will parse the JSON. Open the Terminal tab at the bottom of Android Studio, and enter the following:

dart run build_runner build

When the command completes, if everything ran correctly, the errors you saw earlier in cats.dart will be gone. You’ll now see cats.g.dart in the same directory as cats.dart. If you open cats.g.dart, you’ll notice methods for converting JSON to your model classes and back.

Error Handling

Developers should handle unexpected values from JSON objects. For example, you expect a string type, but the server returns null. This isn’t a rare case where you should leave it as it is. Check the code below:

@JsonSerializable()
class CatBreed {
  String id;
  String url;
  int width;
  int height;

  CatBreed({
    required this.id,
    required this.url,
    required this.width,
    required this.height
  });

  factory CatBreed.fromJson(Map<String, dynamic> json) =>
      _$CatBreedFromJson(json);

  Map<String, dynamic> toJson() => _$CatBreedToJson(this);
}

Cat image, in this case String url, could be null. To avoid any NullPointerException, pass an empty string when String url is null.

You could modify CatBreed.fromJson() to the following:

  factory CatBreed.fromJson(Map<String, dynamic> json) {
    // 1
    try {
      final id =  json['id'] as String;
      final url =  json['url'] as String;
      final width = json['width'] as int;
      final height = json['height'] as int;
      return CatBreed(
        id: id,
        url: url,
        width: width,
        height: height,
      );
    } catch(e) {
      // 2
      return CatBreed(
        id: '',
        url: '',
        width: -1,
        height: -1,
      );
    }
  }

In the code above:

  1. Wraps the fromJson method with the try block to catch any cast exceptions.
  2. The catch block returns a default CatBreed object with all properties being default values.

The code above looks OK, but not elegant. The main drawback of this approach is that if try throws an exception for one property, all properties will be created as a default. The developer doesn’t understand which property is causing the problem.

To fix that, modify CatBreed.fromJson() to the following:

factory CatBreed.fromJson(Map<String, dynamic> json) {
  return CatBreed(
     id: tryCast<String>(json['id']) ?? '',
     url: tryCast<String>(json['url']) ?? '',
     width: tryCast<int>(json['width']) ?? 0,
     height: tryCast<int>(json['height']) ?? 0,
  );
}

Here, you create and return the CatBreed object with default values using the null-coalescing operator (??).

Next, add the tryCast method at the end of cats.dart.

T? tryCast<T>(dynamic object) => object is T ? object : null;

tryCast is a simple method that tries to cast an object into a given type, and if it’s unsuccessful, it returns null.

Now, the code looks elegant, small and easy to read. In the coming sections, you’ll connect the UI with a network response.

Using the Models

Now that you’ve created and generated your models, it’s time to put them to work.

Go back to cat_breeds.dart. In getCatData(), you can now parse the JSON you got from the internet into your model classes.

To start, at the top of _CatBreedsPageState, add a property for the breed list:

class _CatBreedsPageState extends State<CatBreedsPage> {
  BreedList breedList = BreedList(breeds: List.empty());
  ...

Add the import import '../models/cats.dart'; at the top of the file to clear the errors you see.

In getCatData(), add these lines after the print statement:

// 1
final dynamic catMap = json.decode(catJson);
// 2
setState(() {
  // 3
  breedList = BreedList.fromJson(catMap);
});

Here, you:

  1. Use json.decode(catJson) to turn the JSON string into a map.
  2. Call setState to rebuild your widget due to changes in the data.
  3. Use BreedList.fromJson(catMap) to convert the map into a list of breeds.

Be sure to import the dart:convert library(import 'dart:convert';) for the json.decode() statement. You’ve now converted your JSON data into a list of cat breeds!

But wait! You still need to get that list into the UI. How do you do that?

Since you have a list of cat breeds, what better way to display them than with a ListView widget?

Go down to the body: ListView.builder statement and replace itemCount: 0 with:

itemCount: breedList.breeds.length,

This sets itemCount to the number of cat breeds you got from the internet.

Next, replace title and subtitle of ListTile with the following:

title: Text(breedList.breeds[index].name),
subtitle: Text(breedList.breeds[index].description),

Now, build and run the app, and see how it looks. You’ll see a list of cat breed names and their descriptions:

Cat breed list

Congratulations!

Happy Cat