Dart Fundamentals – Working with JSON in Dart and Flutter

In this article I'll teach you how to work with JSON in Dart and Flutter. We'll cover both encoding and decoding. As a refresher, encoding is the act of taking an object instance and representing it as JSON. For example we might take a Student object and encode it as JSON so it looks as follows:

{
   "name":"John Smith",
   "email":"[email protected]",
   "dob":"2000-02-14T00:00:00.000"
}

On the flip side, decoding is taking a JSON string and transforming it into an object that is representative of the data within the JSON string.

Working with JSON in Dart and Flutter

Dart, being the underlying language for any Flutter application is what we will be working in. We'll approach this in a way that keeps us compatible with Flutter. Flutter does not have support for runtime reflection which is how libraries that do magical JSON encoding/decoding work. But the code in this article will be equally compatible with both Dart and Flutter.

Warning - Opinion Ahead!!!

There is also a way to use code generation techniques to handle JSON encoding and decoding. That approach is not covered in this article. I'm honestly not a fan of code generation when it comes to things like this. The proponents of code generation argue that it helps eliminate runtime errors due to mistakes that can be introduced when manually handling your JSON encoding/decoding. I think good unit tests can alleviate this problem without having to markup your models in a specific way or run code generation utilities.

Create a Dart Console Application

Let's get started by creating a brand new Dart console application. If you have not done this before, I have a quick tutorial article here on my blog: Dart Fundamentals – Visual Studio Code and Dart. We could just as easily have created a Flutter app but for no reason in particular I chose to create a Dart console application instead.

The boilerplate code that comes with the console application generated by the Dart VS Code extension looks like the following:
Sample Dart Project

We're just going to ignore the main.dart code as we'll be doing all of our work in unit tests for the remainder of this article. I find that proving out new concepts in unit tests is a very quick/easy way of getting up to speed on a concept.

Create a model

Let's define the model that we are going to work with. I've chosen a very simple Student model that looks as follows:

class Student {
  final String name;
  final String email;
  final DateTime dob;

  Student(this.name, this.email, this.dob);
}    

Create a Service Class to Handle JSON Conversion

Now that we have our Student model, we need to handle the work to encode instances of Students as JSON and vice versa. I prefer to build a separate service class to handle this conversion as opposed to sprinkling JSON encoding/decoding logic within the model classes.

Our code will depend on one of the core Dart libraries - dart:convert. Let's import it at the top of our new service class.

import 'dart:convert';

The two methods we will work with are jsonEncode and jsonDecode.

Encoding with jsonEncode

Encoding our Student instance requires us to define a mapping that maps the Student properties to their equivalent JSON properties. Note that the names in our JSON data don't have to match the names of our Student properties but if they don't make sure you modify the mapping code accordingly.

Map<String, dynamic> map() =>
{
    'name': s.name,
    'email': s.email,
    'dob': s.dob.toIso8601String()
};

After that we simply pass in the results of our mapping to the jsonEncode method. Here is the full code:

  static String toJson(Student s) {
    Map<String, dynamic> map() =>
    {
      'name': s.name,
      'email': s.email,
      'dob': s.dob.toIso8601String()
    };

    String result = jsonEncode(map());
    return result;
  }

Decoding with jsonDecode

When we call jsonDecode we get back a Map of keys that represent the JSON property names and values that represent the data assigned to each property. All we have to do is assign each value to a Student instance.

  static Student fromJson(String jsonString) {
    Map<String, dynamic> json = jsonDecode(jsonString);
    String name = json['name'];
    String email = json['email'];
    DateTime dob = DateTime.parse(json['dob']);
    Student s = new Student(name, email, dob);
    return s;
  }

Write Unit Tests

With our model and service built, we know how our data is represented as well as how we will encode and decode. Let's prove all of this works by spinning up some unit tests.

First up, let's create a sample JSON string that we will use as a reference when checking if we correctly encoded or decoded:

  String json =
      '{"name":"John Smith","email":"[email protected]","dob":"2000-02-14T00:00:00.000"}';

For our first unit test we will make sure we can decode this json string as a Student instance. As you can see, I named the service I just created 'StudentJsonMapper' so you probably have to modify this code to match whatever name you used.

  test('Decode Json', () {
    Student s = StudentJsonMapper.fromJson(json);
    assert(s != null);
    expect(s.name, "John Smith");
    expect(s.email, "[email protected]");
    expect(s.dob, new DateTime(2000, 2, 14));
  });

And to round things out, one more unit test to make sure we are properly encoding a Student instance as a JSON string.

  test('Encode Student', () {
    Student s = new Student(
        "John Smith", "[email protected]", new DateTime(2000, 2, 14));
    var jsonString = StudentJsonMapper.toJson(s);
    expect(json, jsonString);
  });

Running the tests, you should see both tests are passing:

✓ Decode Json
✓ Encode Student
Exited

Nice work! At this point you have the basics of JSON encoding and decoding in Dart and Flutter down. Let's move on to a few more complicated examples. Sometimes we won't get back a single JSON object - many times we will get back an array. Additionally our JSON might have properties that are arrays. We'll discuss both next.

Handling JSON Arrays

Let's assume for our example that we will be getting back JSON that is structured as follows:

{
    "students": [{
            "name": "John Smith",
            "email": "[email protected]",
            "dob": "2000-02-14T00:00:00.000"
        },
        {
            "name": "Jane Blythe",
            "email": "[email protected]",
            "dob": "1999-03-01T00:00:00.000"
        }
    ]
}

To accommodate, we will need to update our mapping service to work with a JSON Array. First we will handle decoding, then encoding.

Decoding a JSON Array

Decoding an array of JSON objects is very similar to decoding a single JSON object. The key difference is that you have to remember you have that Array (List in Dart) to deal with plus each item in your list is a Map<String, dynamic>. The code plus an explanation of how the code works is below:

  //Given a Map (property names are keys, property values are the values), decode as a Student
  static Student fromJsonMap(Map<String, dynamic> json) {
    String name = json['name'];
    String email = json['email'];
    DateTime dob = DateTime.parse(json['dob']);
    Student s = new Student(name, email, dob);
    return s;
  }

  //Given a JSON string representing an array of Students, decode as a List of Student
  static List<Student> fromJsonArray(String jsonString) {
    Map<String, dynamic> decodedMap = jsonDecode(jsonString);
    List<dynamic> dynamicList = decodedMap['students'];
    List<Student> students = new List<Student>();
    dynamicList.forEach((f) {
      Student s = StudentJsonMapper.fromJsonMap(f);
      students.add(s);
    });

    return students;
  }

The first thing we added to our mapping service was a helper method named 'fromJsonMap'. Why we need this method requires an explanation. When the dart:convert library's jsonDecode method encounters a JSON object to decode, it returns it as Map<String, dynamic> where the keys are string representations of the JSON property names and values are whatever value is assigned to the property. The type here is dynamic because the value can be of any supported Dart type (ints, Strings, DateTimes, objects, etc). This helper method makes it easier to pick out the values from this map and use them to create a Student object.

Since in this example we are working with a list of Student JSON objects, we have to remember that each Student is represented as a Map<String, dynamic>. So what we do in the fromJsonArray method is:

  • extract a list of Students
  • iterate that list
  • for each item call fromJsonMap
  • add the student to our result

Write Unit Tests

Lets add in a unit test to prove this works properly.

Here is a test to ensure we can decode a JSON array properly to a list of Students.

  test('Decode JsonArray', () {
    List<Student> list = StudentJsonMapper.fromJsonArray(jsonArray);
    assert(list != null);
    expect(2, list.length);
    expect("John Smith", list[0].name);
    expect("Jane Blythe", list[1].name);
  });

Running the tests, you should see our new test is passing:

✓ Decode JsonArray
Exited

Encoding a List of objects as a JSON Array

In this section we will take a list of Students and encode them as a JSON Array. This code is a litle more straight-forward then decoding. We use the map List.map method to represent the list of Students as a list of Map<String,String>. This becomes our JSON Array of students which we then assign to the "students" property of our JSON object before we pass the whole thing over to jsonEncode:

  //Given a list of students, encode as Json
  static String listToJson(List<Student> students) {
    List<Map<String, String>> x = students
        .map((f) =>
            {'name': f.name, 'email': f.email, 'dob': f.dob.toIso8601String()})
        .toList();

    Map<String, dynamic> map() => {'students': x};
    String result = jsonEncode(map());
    return result;
  }

Write Unit Tests

Let's add in a test to exercise our new listToJson method for converting a list of Students to a JSON array.

  test('Encode Students List', () {
    Student s = new Student(
        "John Smith", "[email protected]", new DateTime(2000, 2, 14));

    Student s2 = new Student("Jane Blythe", "[email protected]", new DateTime(1999,3,1));
    List<Student> list = new List<Student>();
    list.add(s);
    list.add(s2);  

    var jsonString = StudentJsonMapper.listToJson(list);
    expect(jsonArray, jsonString);
  });

And run the test to see it is passing (yay!):

✓ Encode Students List
Exited

Handling Complex JSON data

Sometimes it is useful for JSON data to have properties that are objects themselves. This can make the encoding and decoding of that JSON data a little more complex.

What if our Students JSON data had a list of courses they are registered for? We'd have to make sure we handle that as well. Doing so requires a new model to represent the class.

class Course {
  final int id;
  final String name;
  final String instructor;

  Course(this.id, this.name, this.instructor);
}

Next, lets modify the Student class so we can represent the courses they are registered for:

import 'package:dart_json1/course.dart';

class Student {
  final String name;
  final String email;
  final DateTime dob;
  List<Course> courses;

  Student(this.name, this.email, this.dob);
}

And to make sure we handle encoding/decoding of Courses properly, modifications to our mapping service are required. What we will do is modify our StudentJsonMapper.fromJsonMap method so that it can handle courses for a student:

  //Given a Map (property names are keys, property values are the values), decode as a Student
  static Student fromJsonMap(Map<String, dynamic> json) {
    String name = json['name'];
    String email = json['email'];
    DateTime dob = DateTime.parse(json['dob']);
    Student s = new Student(name, email, dob);

    //Handle courses if present
    if (json.keys.any((a) => a == 'courses')) {
      List<dynamic> dynamicList = json['courses'];
      List<Course> courses = new List<Course>();
      dynamicList.forEach((f) {
        Course c = CourseJsonMapper.fromJsonMap(f);
        courses.add(c);
      });
      s.courses = courses;
    }

    return s;
  }

You'll also see that we introduced a new JSON mapper service for handling courses. This helps us keep the JSON mapping code for students and courses separate. Here is the code for this new service:

abstract class CourseJsonMapper {
  static Course fromJsonMap(Map<String, dynamic> json) {
    int id = json['id'];
    String name = json['name'];
    String instructor = json['instructor'];
    Course c = new Course(id, name, instructor);
    return c;
  }
}

And keeping with our testing philosophy, another unit test is warranted:

  test('Decode jsonArrayWithCourses', () {
    List<Student> list = StudentJsonMapper.fromJsonArray(jsonArrayWithCourses);
    assert(list != null);
    expect(2, list.length);
    expect(2, list.firstWhere((x) => x.name == "John Smith").courses.length);
    expect(1, list.firstWhere((x) => x.name == "Jane Blythe").courses.length);
  });

And run the test to see it is passing:

✓ Decode jsonArrayWithCourses
Exited

That's a Wrap!

The dart:convert package along with our approach to separating out JSON encoding and decoding to service classes makes it fairly easy to work with JSON data. In addition, having ample unit tests proves our mapping is working correctly. I found that when first learning how to do this effectively in Dart and Flutter there was a bit of a learning curve involved but once I got past that it all came together rather nicely. The approaches used here should be readily applicable to most cases of encoding and decoding JSON data.

-Joe

Leave a Reply

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