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
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,
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.
- Least complicated approach — no code is generated for you.
- You can do whatever you want, but you’re on the hook for maintaining it.
- Generates a
fromJsonconstructor and a
toJsonmethod for you.
- You need to include several packages in your project and use
source_gento generate partial files prior to building your app.
- Generated source can be tricky to customize.
- Generates code for serialization, immutability,
hashCodeproperties, and more. It’s a heavyweight solution with a lot of functionality.
json_serializable, you need to import a number of packages and use
- 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.
Converting a JSON string into an intermediate format can be done with the
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.
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:
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.
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.g.dart. You can reference that file in the original using the
part keyword, and the compiler inlines it.
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
- Define a data class as you normally would.
- Add the
@JsonSerializableannotation to the class definition.
- Add some extra bits to link that class with the code
json_serializablecreates for it.
source_gento generate code.
I’ll tackle these one at a time.
json_serializable package into your project
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
Add the @JsonSerializable annotation
json_serializable package only generates code for classes that have been tagged with the
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.
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
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
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 (
- 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
- 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, 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
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:
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
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
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
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.