Some Options for Deserializing JSON with Flutter
At some point, most apps need to reach out and grab data from an online endpoint. Making an HTTPS get
request to pull down a weather report or the World Cup final score is fairly straightforward, thanks to Dart’s http
package:
The data in response.body
is likely a JSON string, and there’s some work to be done before it’s ready for a widget. First, you need to parse the string into a more manageable representation of JSON. Then you must convert that representation into a model or some other strongly typed variable, so you can use it effectively.
Fortunately, the Dart team and community have been slinging JSON around for a while, and can offer solutions. I’ll cover three, in ascending order of complexity: handwritten constructors, json_serializable
, and built_value
.
The calls to deserialize data with all three approaches are very similar. Handwritten constructors and json_serializable
result in lines like this one:
And a deserialization call for built_value
looks like this:
The real difference is in how much code within that `SimpleObject` class is generated on your behalf, and what it’s doing.
Handwritten constructors
- Least complicated approach — no code is generated for you.
- You can do whatever you want, but you’re on the hook for maintaining it.
json_serializable
- Generates a
fromJson
constructor and atoJson
method for you. - You need to include several packages in your project and use
source_gen
to generate partial files prior to building your app. - Generated source can be tricky to customize.
built_value
- Generates code for serialization, immutability,
toString
methods,hashCode
properties, and more. It’s a heavyweight solution with a lot of functionality. - Like
json_serializable
, you need to import a number of packages and usesource_gen
. - Has an extensible, plugin-based serialization architecture.
- Has opinions about things like instance creation and mutability.
As you’ll see, which library is right for you really depends on the details of your project, in particular its size and method of state management. A hobby project with one maintainer can go far with handwritten constructors, while an app built by a big, distributed team that needs immutable models to keep their logic clean can really benefit from `built_value`.
For now, though, let’s start at the beginning: parsing JSON from a string into a more convenient in-memory representation. This step in the process is the same regardless of which way you decide to go later on.
Parsing JSON
Converting a JSON string into an intermediate format can be done with the dart:convert
library:
If the string contains valid JSON, you’ll get back a dynamic
reference to either a List<dynamic>
or a Map<String, dynamic>
depending on whether the JSON string held an array or a single object. For simple things like a list of integers, you’re pretty much done at this point. You’ll probably want to create a second, strongly typed reference to the data, though, before you use it:
More sophisticated payloads are where things get interesting. Converting a Map<String, dynamic>
into an actual model object can involve casting, default values, nulls, and nested objects. There are many ways things can go wrong, and many annoying details that need to be updated if you later decide to rename or add/remove a property.
Handwritten constructors
One has to start somewhere, right? If you have a small app and the data is not that complicated, you can go a long way by writing your own factory constructors that take a Map<String, dynamic>
parameter. For example, if you’re fetching that looks like this:
The code for a matching class might look like this:
The named fromJson
factory constructor is then used like this:
On the downside, you had to write ~20 lines of constructor code by hand, and you’re now on the hook for maintaining it. As your app grows and your data classes start to number in the dozens, you might find yourself thinking, “Man, coding these JSON constructors is getting tedious — I wish they could just be auto-generated from the properties in the class.”
As it turns out, with the json_serializable
library, they can.
Using json_serializable
Before getting into json_serializable
, we need to take a brief detour to talk about another package.
Flutter doesn’t currently offer support for reflection, so some techniques available in other contexts (such as the ability of an Android JSON library to inspect classes for annotations at runtime) won’t work for Flutter devs. What they can use, however, is a Dart package called source_gen
. It provides utilities and a basic framework for automated source code generation.
Rather than updating your code directly, source_gen
creates new, separate files alongside them. By convention, they’re given a “g” in their filenames, so if your class resides in model.dart
, source_gen
creates model.g.dart
. You can reference that file in the original using the part
keyword, and the compiler inlines it.
The json_serializable
package uses the source_gen
API to generate serialization code. It writes the fromJson
constructor (as well as a toJson
method) for you.
The basic process for putting it to work in an app looks like this:
- Import into your project the
json_serializable
andjson_annotation
packages. - Define a data class as you normally would.
- Add the
@JsonSerializable
annotation to the class definition. - Add some extra bits to link that class with the code
json_serializable
creates for it. - Run
source_gen
to generate code.
I’ll tackle these one at a time.
Import the json_serializable
package into your project
You can find json_serializable
in the Dart package catalog. Just update your pubspec.yaml
as directed, and you’re good to go.
Define a data class
No surprises here. Make a data class with basic properties and a constructor. The properties you plan to serialize should either be value types or other classes made to work with json_serializable
.
Add the @JsonSerializable annotation
The json_serializable
package only generates code for classes that have been tagged with the @JsonSerializable
annotation:
Link the generated code with yours
Next up are three changes that wire the class definition to its corresponding part file:
The first of these is the part
declaration, which tells the compiler to inline simple_object.g.dart
(more on that in a minute). Then there’s an update to the class definition to use a mixin. Finally, update the class to use the fromJson
constructor. The last two changes each reference code in the generated file.
Run source_gen
Kick off code generation from your project folder with this command:
flutter packages pub run build_runner build
When finished, you’ll have a new file called simple_object.g.dart
sitting alongside the original. The contents look like this:
That first method is called by the fromJson
constructor in SimpleObject
and the mixin class provides the new toJson
method to SimpleObject
. Both are straightforward to use:
In terms of numbers, adding three lines of code to simple_object.dart
for json_serializable
saves about 20 lines of constructor code that you’d otherwise have to write. You’re also able to regenerate it anytime you want to rename or otherwise adjust the properties, and you get a toJson
method thrown in for free. Not too shabby.
What if you’d like to serialize to more than one format, though? Or to more than just JSON? And what if you need other things, like immutable model objects? For those use cases, there’s built_value
.
Using built_value
Much more than just a solution for automating serialization logic, built_value
(along with its partner package, built_collection
) is designed to help you create classes that function as value types. For this reason, instances of classes created with built_value
are immutable. You can create new instances (including copies of existing ones), but once they’re built, their properties can’t be changed.
To accomplish this, built_value
uses the same source generation approach found in json_serializable
, but creates a lot more code. In the generated file for a built_value
class, you’ll find:
- An equality (
==
) operator - A
hashCode
property - A
toString
method - A serializer class, if you want one — more on that below
- A “builder” class used to make new instances
This adds up to several hundred lines, even for a small class like SimpleObject, so I’ll avoid showing it here. The actual class file (what you’d write as a dev) looks like this:
The differences between this and the version of SimpleObject
we started with are:
- A part file is declared, just like
json_serializable
. - An interface,
Built<SimpleObject, SimpleObjectBuilder>
, is being implemented. - A static getter for a serializer object has been added.
- Nullability annotations are on all the fields. These are optional, but to make this example match the others in functionality, I’ve added them.
- Two constructors (one private, one factory) have been added, and the original one was removed.
- SimpleObject is now an abstract class!
The differences between this and the version of SimpleObject
we started with are:
Let’s start with the last point: SimpleObject
has become an abstract class. In its generated file, built_value
defines a subclass of SimpleObject
called _$SimpleObject
, and that’s where it provides a lot of the new functionality. It’s where you’ll find the new hashCode
property, new methods relating to immutability, and so on. Each time you create an instance of SimpleObject
, you’re actually getting _$SimpleObject
under the hood. You’ll never need to reference it by the derived type, though, so your app code still uses SimpleObject
to declare and use references.
This is possible because instantiation of a brand-new SimpleObject
is done through a generated factory constructor, which you can see referenced in the last line of the file above. To use it, you pass in a method that sets properties on a SimpleObjectBuilder
(it’s the “b” parameter below), which builds the immutable object instance for you:
You can also rebuild
to get a modified bopy of an existing instance:
As you can see, the constructor in SimpleObject
has been made private through the use of an underscore:
That guarantees that your app’s code can’t directly instantiate an instance of SimpleObject
. In order to get one, you have to use the factory constructor, which uses SimpleObjectBuilder
and always produces an instance of the _$SimpleObject
subclass.
That’s great, but I thought we were talking about deserialization?
I’m getting to that! To serialize and deserialize instances, you’ll need to add a little code somewhere in the app (creating a file called serializers.dart
, for example, is a good approach):
This file does two things. First, it uses the @SerializersFor
annotation to instruct built_value
to create serializers for a list of classes. In this case, there’s only one class, so it’s a short list. Second, it creates a global variable called serializers
that references the Serializers
object that handles serialization of built_value
classes. It’s used like this:
As with json_serializable
, transforming an object into and out of JSON is still mostly a one line affair, with the generated code doing the heavy lifting for you. One important thing to note, though, is this bit from serializers.dart
:
built_value
is designed to be as extensible as possible, and it includes a plugin system for defining custom serialization formats (you could, for example, write one to translate to and from XML or your own binary format). I’m using it in this example to add a plugin called StandardJsonPlugin
because, by default, built_value
doesn’t use the map-based JSON format that you’re probably used to.
Instead, it uses a list-based format. For example, a simple object with String
, int
, and double
members would be serialized like this:
Rather than this:
There are a few reasons why built_value
prefers the list-based format, which for sake of space I’ll leave to the package documentation. For this example, though, just know that you can easily make use of map-based JSON serialization through the StandardJsonPlugin
, which ships as part of the built_value
package.
Conclusion
So those are the high points of all three techniques. As I mentioned back at the beginning of this article, choosing the right one is mostly about considering the scope of your project, how many people are working on it, and what other needs you have for your model objects.
The next step is to start coding, so come see us at the Flutter Dev Google Group, StackOverflow, or The Boring Show, and let us know how it goes!