Heads up: This post originally appeared on BradCypert.com but has been reformatted on Dev.to for your viewing pleasure.
JSON is, as of 2020, the communication standard for most web applications (comon gRPC! You can do it!). Naturally, if you’re building a Dart application, you’ll likely want to work with JSON. Dart’s built-in dart:convert package is just the tool that you need!
Creating a Model
One of the first things that you’ll probably want to do is to create a model for your JSON data. In this example, we’ll use a simplified version of the Github API’s repository request. If you’d like to inspect the response that we’ll be parsing, here’s the URL : https://api.github.com/users/bradcypert/repos
We’ll start by creating a simple Dart class to model this response. I say “simple” because we’re not going to model all of the fields in that response — we don’t need all that data and it’d be a lot of code that might dilute the quality of this demonstration. We’ll also override the toString
method for ease of printing our new model.
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
That’ll work. We’ll just capture the id
, name
, description
, and url
for our repositories. Now that we have something to store our decoded JSON into, the next step is to actually decode the JSON. We’ll leverage a few Dart packages for these steps. Here’s the breakdown of what we’re going to do:
1. We need to make an HTTP request to the github repository list endpoint.
1. We need to decode the response body into a `dynamic` type for our JSON.
1. We need to use that `dynamic` JSON type to create instances of our Repository Model.
Let’s get to it!
import 'package:http/http.dart' as http;
import 'dart:convert'; // give us the global jsonDecode
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
void main() {
http.get("https://api.github.com/users/bradcypert/repos")
.then((response) => jsonDecode(response.body)).then((jsonBody) {
var repos = jsonBody.map((repo) =>
Repository(
id: repo['id'],
name: repo['name'],
description: repo['description'],
url: repo['url']
)
);
repos.forEach((repo) => print(repo.toString() + "\n"));
});
}
jsonDecode
is doing all of the heavy lifting in regards to the parsing. We request the data from github, parse it, and then used the parsed JSON to create our model. Finally, for the sake of proving it works, we print our model to the console. Truncated for brevity, you should see something like this in your console:
[199228305, async-http, A channel-based HTTP client built around Golang's net/http package., https://api.github.com/repos/bradcypert/async-http]
[290240968, bradcypert.com, WordPress theme for BradCypert.com, https://api.github.com/repos/bradcypert/bradcypert.com]
[44900977, clojchimp, A small MailChimp client written in Clojure, https://api.github.com/repos/bradcypert/clojchimp]
...
First, pat yourself on the back. You’re parsing JSON with Dart. Second, don’t you think we could make this a little cleaner? Me too. Let’s create a new constructor for our Model.
Refactoring our Model
We’re going to add redirecting constructor to our model here. This allows us to maintain our primary constructor, but specify a new constructor specifically for dealing with dynamic
json types of data.
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
Repository.fromJSON(dynamic json) {
this(
id: repo['id'],
name: repo['name'],
description: repo['description'],
url: repo['url']
)
}
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
Now, with our new redirecting constructor, we can simplify our main method as well!
import 'package:http/http.dart' as http;
import 'dart:convert'; // give us the global jsonDecode
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
Repository.fromJSON(dynamic json) : this(
id: json['id'],
name: json['name'],
description: json['description'],
url: json['url']
);
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
void main() {
http.get("https://api.github.com/users/bradcypert/repos")
.then((response) => jsonDecode(response.body)).then((jsonBody) {
var repos = jsonBody.map((jsonRepo) =>
Repository.fromJSON(jsonRepo)
);
repos.forEach((repo) => print(repo.toString() + "\n"));
});
}
How about that? Same output, but cleaner. Note: I know that you can simplify this even further by removing the .map
call and just creating the Repository in the forEach
instead. Most real world applications don’t just print to the console each of the items they have, and I find that proper use of .map
tends to lead towards cleaner code. For demo purposes, you can simplify it, but follow I suggest following the patterns with .map
in your own application!
Generating JSON from Models
Now that we know how to decode JSON and generate class instances from that JSON, it’s time that we flip-the-script and do the opposite. We’ll take our Repository class and modify it so that it can generate a Map that we can parse via jsonEncode
. So, our steps for this change are as follows:
- Add a new method to our model called
.toJson
- Write the code to return a map containing key-value pairs to represent our model’s data.
- Call
jsonEncode
passing in our Repository instance (toJson
method is called bejsonEncode
under the hood).
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
Repository.fromJSON(dynamic json) : this(
id: json['id'],
name: json['name'],
description: json['description'],
url: json['url']
);
Map<String, dynamic> toJson() => {
'id': this.id,
'name': this.name,
'description': this.description,
'url': this.url
};
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
So now that we’ve added our toJson
method, we can call jsonEncode
on a repository object like so:
import 'package:http/http.dart' as http;
import 'dart:convert'; // give us the global jsonDecode
class Repository {
final int id;
final String name;
final String description;
final String url;
Repository({this.id, this.name, this.description, this.url});
Repository.fromJSON(dynamic json) : this(
id: json['id'],
name: json['name'],
description: json['description'],
url: json['url']
);
Map<String, dynamic> toJson() => {
'id': this.id,
'name': this.name,
'description': this.description,
'url': this.url
};
String toString() {
return '[${this.id}, ${this.name}, ${this.description}, ${this.url}]';
}
}
void main() {
// create a new Repository instance in our application
Repository repo = Repository(id: 0, name: "test repo", description: "No thanks.", url: "github.com/nextawesomeproject");
// print the repo as is (calling toString under the hood)
print(repo);
// print the repo as valid JSON
print(jsonEncode(repo));
}
And that’ll do it! You’re now encoding and decoding JSON in Dart! Best of luck!
PS: You can TOTALLY make this a tiny bit simpler (or maybe a lot simpler when working with big data structures) by using reflection. Dart’s runtime reflection package (called Mirrors) is actually pretty rad, however it can not be used with Flutter (due to it breaking Flutter’s tree-shaking mechanisms). If you’re using Dart with something other than Flutter please let me know what you’re working on see the reflection package here.
Top comments (0)