Work with JSON in Flutter - Part 1: dart:convert

6 min read, 10 July, 2018

When building Flutter apps, it's inevitable to deal with JSON data. JSON is the common data format for communication with back-end servers or other services. However, using JSON in Flutter is not as easy as other platforms. There are some limitations. In this series, we are going to explore two different ways to work with JSON in Flutter.

Let's start from the simplest one - using dart:convert.

Basics

dart:convert provides two functions json.decode and json.encode to work with JSON data.

Decode

Below is an example of JSON data of a user generated with mockaroo.

{
  "id": 1,
  "firstName": "Kariotta",
  "lastName": "Ginley",
  "email": "kginley0@domainmarket.com",
  "vip": true,
  "shippingAddresses": [
    {
      "address": "31 Coolidge Point",
      "city": "Amarillo",
      "state": "Texas",
      "country": "United States",
      "zipcode": "79105"
    },
    {
      "address": "4 Linden Center",
      "city": "Apache Junction",
      "state": "Arizona",
      "country": "United States",
      "zipcode": "85219"
    }
  ],
  "dateOfBirth": "3/6/1983"
}

To parse this JSON data, we can simply import the library dart:convert and use the function json.decode to parse it.

import 'dart:convert';

var jsonStr = """
{
  "id": 1,
  "firstName": "Kariotta",
  "lastName": "Ginley",
  "email": "kginley0@domainmarket.com",
  "vip": true,
  "shippingAddresses": [
    {
      "address": "31 Coolidge Point",
      "city": "Amarillo",
      "state": "Texas",
      "country": "United States",
      "zipcode": "79105"
    },
    {
      "address": "4 Linden Center",
      "city": "Apache Junction",
      "state": "Arizona",
      "country": "United States",
      "zipcode": "85219"
    }
  ],
  "dateOfBirth": "3/6/1983"
}
""";

void main() {
  var result = json.decode(jsonStr);
  print(result['id']); // 1
  print(result['firstName']); // 'Kariotta'
  print('Type of shippingAddresses is ${result['shippingAddresses'].runtimeType}'); // Type of shippingAddresses is List
}

The type of the return value result is Map<String, dynamic>. It's a Map because we are parsing a JSON object. The type of Map key is always String, while the type of Map value depends on the actual data, so it's dynamic. We can use runtimeType to get the actual type of value for the key shippingAddresses, which is List.

We can also parse arrays, string, numbers, booleans and nulls in the JSON.

import 'dart:convert';

var jsonStr = """
["a", "b", "c"]
""";

void main() {
  var result = json.decode(jsonStr);
  outputType(result); // Type is List
  outputType(json.decode('"hello"')); // Type is String
  outputType(json.decode('123')); // Type is int
  outputType(json.decode('true')); // Type is bool
  outputType(json.decode('null')); // Type is Null
}

void outputType(dynamic obj) {
  print('Type is ${obj.runtimeType}');
}

Encode

Encoding is the opposite of decoding and we use the function json.encode to generate the JSON string.

import 'dart:convert';

void main() {
  print(json.encode('123')); // "123"
  print(json.encode('"Hello"')); // "\"Hello\""
  print(json.encode(true)); // true
  print(json.encode(null)); // null
  print(json.encode([1, 2, 3])); // [1,2,3]
  var values = {
    'a': 1,
    'b': 2,
    'c': 3
  };
  print(json.encode(values)); // {"a":1,"b":2,"c":3}
}

json.encode only supports simple values String, num, bool, Null and List and Map by default. List and Map can only contain simple values or a combination of List and Map with simple values. If you try to pass a custom object to json.encode, it throws an exception.

The code below is trying to encode an object of custom class Demo.

import 'dart:convert';

class Demo {
  String a;
  int b;
  bool c;
  Demo({this.a, this.b, this.c});
}

void main() {
  var demo = new Demo(a: 'hello', b: 100, c: false);
  print(json.encode(demo));
}

The code above fails with following error.

Unhandled exception:
Converting object to an encodable object failed: Instance of 'Demo'

For unknown/custom objects, json.encode will try to invoke the function toJson on the object and use it as the value for encoding. To make the class Demo encodable, we need to add the function toJson. toJson simply returns a Map with all properties. The type of Map keys must be String. Now json.encode successfully returns the JSON string {"a":"hello","b":100,"c":false}.

class Demo {
  String a;
  int b;
  bool c;
  Demo({this.a, this.b, this.c});

  toJson() {
    return {
      'a': a,
      'b': b,
      'c': c,
    };
  }
}

Work with classes

In real-world Flutter apps development, we'll typically use custom classes instead of Map or List objects. For the JSON data listed above, we can create a class User to represent it. We also need a class Address for addresses.

class User {
  int id;
  String firstName, lastName, email;
  bool vip;
  String dateOfBirth;
  List<Address> shippingAddresses;

  User({
    this.id,
    this.firstName,
    this.lastName,
    this.email,
    this.vip,
    this.dateOfBirth,
    this.shippingAddresses,
  });
}

class Address {
  String address, city, state, country, zipcode;

  Address({
    this.address,
    this.city,
    this.state,
    this.country,
    this.zipcode,
  });
}

To create new User objects from JSON data, we can add a new factory constructor fromJson for both User and Address. The type of shippingAddresses is List, so we can map its elements into objects of Address.

class User {
  int id;
  String firstName, lastName, email;
  bool vip;
  String dateOfBirth;
  List<Address> shippingAddresses;

  User({
    this.id,
    this.firstName,
    this.lastName,
    this.email,
    this.vip,
    this.dateOfBirth,
    this.shippingAddresses,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return new User(
      id: json['id'],
      firstName: json['firstName'],
      lastName: json['lastName'],
      email: json['email'],
      vip: json['vip'],
      dateOfBirth: json['dateOfBirth'],
      shippingAddresses: json['shippingAddresses'].map((value) => new Address.fromJson(value)).toList()
    );
  }
}

class Address {
  String address, city, state, country, zipcode;

  Address({
    this.address,
    this.city,
    this.state,
    this.country,
    this.zipcode,
  });

  factory Address.fromJson(Map<String, dynamic> json) {
    return new Address(
        address: json['address'],
        city: json['city'],
        state: json['state'],
        country: json['country'],
        zipcode: json['zipcode']);
  }
}

Now we can parse JSON data into User objects.

import 'dart:convert';
import './user.dart';

var jsonStr = """
{
  <json_data>
}
""";

void main() {
  var result = json.decode(jsonStr);
  var user = new User.fromJson(result);
  print(user);
}

Summary

From the usage patterns shown above, we can see clearly when should dart:convert be used. dart:convert is very simple and intuitive to use. However, it's not very convenient to extract data from the parsed Map, especially for nested JSON objects. For each nested object, we need to check for nulls first, then continue to extract properties in that object. In the code above, we should check if shippingAddresses is null before accessing it when shippingAddresses is nullable.

The usage of dart:convert is very flexible. If the format of JSON data is different from the model used in the app, we can always put the transformation logic in the factory constructor fromJson and the function toJson. For the user JSON data listed above, the class User may only have one property name, which should be the combination of firstName and lastName in the JSON data. We can simply update fromJson to provide the desired value.

dart:convert should only be used for simple JSON parsing scenarios, primarily for proof of concepts and demos.

Source code

Source code of this article is available on GitHub.