Home Flutter Books Flutter Apprentice

10
Serialization With JSON Written by Kevin D Moore

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In this chapter, you’ll learn how to serialize JSON data into model classes. A model class represents data for a particular object. An example is a recipe model class, which usually has a title, an ingredient list and steps to cook it.

You’ll continue with the previous project, which is the starter project for this chapter, and you’ll add a class that models a recipe and its properties. Then you’ll integrate that class into the existing project.

By the end of the chapter, you’ll know:

  • How to serialize JSON into model classes.
  • How to use Dart tools to automate the generation of model classes from JSON.

What is JSON?

JSON, which stands for JavaScript Object Notation, is an open-standard format used on the web and in mobile clients. It’s the most widely used format for Representational State Transfer (REST)-based APIs that servers provide (https://en.wikipedia.org/wiki/Representational_state_transfer). If you talk to a server that has a REST API, it will most likely return data in a JSON format. An example of a JSON response looks something like this:

{
  "recipe": {
    "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_b79327d05b8e5b838ad6cfd9576b30b6",
    "label": "Chicken Vesuvio"
  }
}

That is an example recipe response that contains two fields inside a recipe object.

While it’s possible to treat the JSON as just a long string and try to parse out the data, it’s much easier to use a package that already knows how to do that. Flutter has a built-in package for decoding JSON, but in this chapter, you’ll use the json_serializable and json_annotation packages to help make the process easier.

Flutter’s built-in dart:convert package contains methods like json.decode and json.encode, which converts a JSON string to a Map<String, dynamic> and back. While this is a step ahead of manually parsing JSON, you’d still have to write extra code that takes that map and puts the values into a new class.

The json_serializable package comes in handy because it can generate model classes for you according to the annotations you provide via json_annotation. Before taking a look at automated serialization, you’ll see in the next section what manual serialization entails.

Writing the code yourself

So how do you go about writing code to serialize JSON yourself? Typical model classes have toJson() and fromJson() methods, so you’ll start with those.

class Recipe {
  final String uri;
  final String label;

  Recipe({this.uri, this.label});
}
factory Recipe.fromJson(Map<String, dynamic> json) {
  return Recipe(json['uri'] as String, json['label'] as String);
}

Map<String, dynamic> toJson() {
  return <String, dynamic>{ 'uri': uri, 'label': label}
}

Automating JSON serialization

You’ll use two packages in this chapter: json_annotation and json_serializable from Google.

Adding the necessary dependencies

Open the starter project in the projects folder. Add the following package to pubspec.yaml in the Flutter dependencies section underneath and aligned with shared_preferences: ^2.0.5:

json_annotation: ^3.1.1
build_runner: ^1.10.0
json_serializable: ^3.5.1
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  cached_network_image: ^2.5.0
  flutter_slidable: ^0.5.7
  flutter_svg: ^0.19.3
  shared_preferences: ^2.0.5
  json_annotation: ^3.1.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^1.10.0
  json_serializable: ^3.5.1

Generating classes from JSON

The JSON that you’re trying to serialize looks something like:

{
  "q": "pasta",
  "from": 0,
  "to": 10,
  "more": true,
  "count": 33060,
  "hits": [
    {
      "recipe": {
        "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_09b4dbdf0c7244c462a4d2622d88958e",
        "label": "Pasta Frittata Recipe",
        "image": "https://www.edamam.com/web-img/5a5/5a5220b7a65c911a1480502ed0532b5c.jpg",
        "source": "Food Republic",
        "url": "http://www.foodrepublic.com/2012/01/21/pasta-frittata-recipe",
    }
  ]
}

Creating model classes

Start by creating a new directory named network in the lib folder. Inside this folder, create a new file named recipe_model.dart. Then add the needed imports:

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'recipe_model.g.dart';
@JsonSerializable()
class APIRecipeQuery {
}
final bool nullable;

/// Creates a new [JsonSerializable] instance.
const JsonSerializable({
  this.anyMap,
  this.checked,
  this.createFactory,
  this.createToJson,
  this.disallowUnrecognizedKeys,
  this.explicitToJson,
  this.fieldRename,
  this.ignoreUnannotated,
  this.includeIfNull,
  this.nullable,
  this.genericArgumentFactories,
});

Converting to and from JSON

Now, return to recipe_model.dart and add these methods for JSON conversion within the APIRecipeQuery class:

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

Map<String, dynamic> toJson() => _$APIRecipeQueryToJson(this);
@JsonKey(name: 'q')
String query;
int from;
int to;
bool more;
int count;
List<APIHits> hits;
APIRecipeQuery({
  @required this.query,
  @required this.from,
  @required this.to,
  @required this.more,
  @required this.count,
  @required this.hits,
});
// 1
@JsonSerializable()
class APIHits {
  // 2
  APIRecipe recipe;

  // 3
  APIHits({
    @required this.recipe,
  });

  // 4
  factory APIHits.fromJson(Map<String, dynamic> json) =>
      _$APIHitsFromJson(json);
  Map<String, dynamic> toJson() => _$APIHitsToJson(this);
}
@JsonSerializable()
class APIRecipe {
  // 1
  String label;
  String image;
  String url;
  // 2
  List<APIIngredients> ingredients;
  double calories;
  double totalWeight;
  double totalTime;

  APIRecipe({
    @required this.label,
    @required this.image,
    @required this.url,
    @required this.ingredients,
    @required this.calories,
    @required this.totalWeight,
    @required this.totalTime,
  });

  // 3
  factory APIRecipe.fromJson(Map<String, dynamic> json) =>
      _$APIRecipeFromJson(json);
  Map<String, dynamic> toJson() => _$APIRecipeToJson(this);
}

// 4
String getCalories(double calories) {
  if (calories == null) {
    return '0 KCAL';
  }
  return calories.floor().toString() + ' KCAL';
}

// 5
String getWeight(double weight) {
  if (weight == null) {
    return '0g';
  }
  return weight.floor().toString() + 'g';
}
@JsonSerializable()
class APIIngredients {
  // 1
  @JsonKey(name: 'text')
  String name;
  double weight;

  APIIngredients({
    @required this.name,
    @required this.weight,
  });

  // 2
  factory APIIngredients.fromJson(Map<String, dynamic> json) =>
      _$APIIngredientsFromJson(json);
  Map<String, dynamic> toJson() => _$APIIngredientsToJson(this);
}

Generating the .part file

Open the terminal in Android Studio by clicking on the panel in the lower left, or by selecting View ▸ Tool Windows ▸ Terminal, and type:

flutter pub run build_runner build
[INFO] Generating build script...
...
[INFO] Creating build script snapshot......
...
[INFO] Running build...
...
[INFO] Succeeded after ...
flutter pub run build_runner watch
// 1
APIRecipeQuery _$APIRecipeQueryFromJson(Map<String, dynamic> json) {
  return APIRecipeQuery(
    // 2
    query: json['q'] as String,
    // 3
    from: json['from'] as int,
    to: json['to'] as int,
    more: json['more'] as bool,
    count: json['count'] as int,
    // 4
    hits: (json['hits'] as List)
        ?.map((e) =>
            e == null ? null : APIHits.fromJson(e as Map<String, dynamic>))
        ?.toList(),
  );
}

Testing the generated JSON code

Now that you have the ability to parse model objects from JSON, you’ll read one of the JSON files included in the starter project and show one card to make sure you can use the generated code.

import 'dart:convert';
import '../../network/recipe_model.dart';
import 'package:flutter/services.dart';
import '../recipe_card.dart';
APIRecipeQuery _currentRecipes1;
Future loadRecipes() async {
  // 1
  final jsonString = await rootBundle.loadString('assets/recipes1.json');
  setState(() {
    // 2
    _currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
  });
}
@override
void initState() {
  super.initState();
  loadRecipes();
  // ... rest of method
}
import 'recipe_details.dart';
Widget _buildRecipeCard(BuildContext context, List<APIHits> hits,
    int index) {
  // 1
  final recipe = hits[index].recipe;
  return GestureDetector(
    onTap: () {
      Navigator.push(context, MaterialPageRoute(
        builder: (context) {
          return const RecipeDetails();
        },
      ));
    },
    // 2
    child: recipeStringCard(recipe.image, recipe.label),
  );
}
Widget _buildRecipeLoader(BuildContext context) {
  // 1
  if (_currentRecipes1 == null || _currentRecipes1.hits == null) {
    return Container();
  }
  // Show a loading indicator while waiting for the recipes
  return Center(
    // 2
    child: _buildRecipeCard(context, _currentRecipes1.hits, 0),
  );
}

Key points

  • JSON is an open-standard format used on the web and in mobile clients, especially with REST APIs.
  • In mobile apps, JSON code is usually parsed into the model objects that your app will work with.
  • You can write JSON parsing code yourself, but it’s usually easier to let a JSON package generate the parsing code for you.
  • json_annotation and json_serializable are packages that will let you generate the parsing code.

Where to go from here?

In this chapter, you’ve learned how to create models that you can parse from JSON and then use when you fetch JSON data from the network. If you want to learn more about json_serializable, go to https://pub.dev/packages/json_serializable.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.