Parsing JSON is a very common task for apps that need to fetch data from the Internet.
And depending on how much JSON data you need to process, you have two options:
- write all the JSON parsing code manually
- automate the process with code generation
This guide will focus on how to manually parse JSON to Dart code, including:
- encoding and decoding JSON
- defining type-safe model classes
- parsing JSON to Dart code using a factory constructor
- dealing with nullable/optional values
- data validation
- serializing back to JSON
- parsing complex/nested JSON data
- picking deep values with the deep_pick package
By the end of this article, you'll learn how to write model classes with robust JSON parsing and validation code.
And in the next article, you'll learn about JSON parsing with code generation tools, so that you don't have to write all the parsing code by hand.
sponsor
Build Privacy-First Flutter apps with the @platform. Our Open Source platform, which is built on Dart, gives people control over their own data. Automatically comply with GDPR [and other privacy regulations], earn commissions as you grow, and make apps super fast no backend infrastructure needed.
We have a lot to cover, so let's start from the basics.
Anatomy of a JSON document
If you worked with any REST APIs before, this sample JSON response should look familiar:
This simple document represents a map of key-value pairs where:
- the keys are strings
- the values can be any primitive type [such as a boolean, number, or string], or a collection [such as a list or map]
JSON data can contain both maps of key-value pairs [using {}] and lists [using []]. These can be combined to create nested collections that represent complex data structures.
In fact, our example includes a list of reviews for a given restaurant:
These are stored as a list of maps for the reviews key, and each review is a valid JSON fragment in itself.
Encoding and Decoding JSON
When a JSON response is sent over the network, the entire payload is encoded as a string.
But inside our Flutter apps we don't want to manually extract the data from a string:
Instead, we can read the contents by decoding the JSON.
To send JSON data over the network, it first needs to be encoded or serialized. Encoding is the process of turning a data structure into a string. The opposite process is called decoding or deserialization. When you receive a JSON payload as a string, you need to decode or deserialize it before you can use it.
Decoding JSON with dart:convert
For simplicity, let's consider this small JSON payload:
To read the keys and values inside it, we first need to decode it using the dart:convert package:
If we run this code, we get this output:
In practice, the result type is the same as Map.
_InternalLinkedHashMap is an private implementation of LinkedHashMap, which in turn implements Map.
So the keys are of type String and the values are of type dynamic. This makes sense because each JSON value could be a primitive type [boolean/number/string], or a collection [list or map].
In fact, jsonDecode[] is a generic method that works on any valid JSON payload, regardless of what's inside it. All it does is decode it and return a dynamic value.
But if we work with dynamic values in Dart we lose all the benefits of strong type-safety. A much better approach is to define some custom model classes that represent our response data, on a case-by-case basis.
Since Dart is a statically-typed language, it's important to convert JSON data into model classes that represent real-world objects [such as a recipe, an employee, etc], and make the most of the type system.
So let's see how to do this.
Parsing JSON to a Dart model class
Given this simple JSON:
We can write a Restaurant class to represent it:
As a result, rather than reading the data like this:
we can read it like this:
This is much cleaner and we can leverage the type system to get compile-time safety and avoid typos and other mistakes.
However, we haven't specified how to convert our parsedJson to a Restaurant object yet!
JSON to Dart: Adding a factory constructor
Let's define a factory constructor to take care of this:
A factory constructor is a good choice for JSON parsing as it lets us do some work [create variables, perform some validation] before returning the result. This is not possible with regular [generative] constructors.
Since the values of our map are dynamic, we explicitly cast them to our desired type [String in this case]. This is a good practice that can be enforced by using the recommended lint rules.
This is how we can use our constructor:
Much better. Now the rest of our code can use Restaurant and get all the advantages of strong type-safety in Dart.
JSON to Dart with Null Safety
Sometimes we need to parse some JSON that may or may not have a certain key-value pair.
For example, suppose we have an optional field telling us when a restaurant was first opened:
If the year_opened field is optional, we can represent it with a nullable variable in our model class.
Here's an updated implementation for the Restaurant class:
As a general rule, we should map optional JSON values to nullable Dart properties. Alternatively, we can use non-nullable Dart properties with a sensible default value, like in this example:
Note how in this case we use the null-coalescing operator [??] to provide a default value.
Data validation
One benefit of using factory constructors is that we can do some additional validation if needed.
For example, we could write some defensive code that throws an UnsupportedError if a required value is missing.
In general, it's our job as the API consumer to work out, for each value:
- its type [String, int, etc.]
- if it's optional or not [nullable vs non-nullable]
- what range of values are allowed
This will make our JSON parsing code more robust. And we won't have to deal with invalid data in our widget classes, because all the validation is done upfront.
To make your code production-ready, consider writing unit tests to test all possible edge cases for all your model classes.
JSON Serialization with toJson[]
Parsing JSON is useful, but sometimes we want to convert a model object back to JSON and send it over the network.
To do this, we can define a toJson[] method for our Restaurant class:
And we can use this like so:
Parsing Nested JSON: List of Maps
Now that we understand the basics of JSON parsing and validation, let's go back to our initial example and see how to parse it:
We want to use model classes and type-safety all the way, so let's define a Review class:
Then we can update the Restaurant class to include a list of reviews:
And we can also update the factory constructor:
Here's a breakdown of all the new code:
- the reviews may be missing, hence we cast to a nullable List
- the values in the list could have any type, so we use List
- we use the .map[] operator to convert each dynamic value to a Review object using Review.fromJson[]
- if the reviews are missing, we use an empty list [[]] as a fallback
This specific implementation makes some assumptions about what may or may not be null, what fallback values to use etc. You need to write the parsing code that is most appropriate for your use case.
Serializing Nested Models
As a last step, here's the toJson[] method to convert a Restaurant [and all its reviews] back into a Map:
Note how we convert the List back to a List, as we need to serialize all nested values as well [and not just the Restaurant class itself].
With the code above, we can create a Restaurant object and convert it back into a map that can be encoded and printed or sent over the network:
Picking Deep Values
Parsing a whole JSON document into type-safe model classes is a very common use case.
But sometimes we just want to read some specific values that may be deeply nested.
Let's consider our sample JSON once again:
If we wanted to get the score for the first review, we could do it like this:
This is valid Dart code because the decodedJson variable is dynamic and we can use the subscript operator with it [[]].
But the code above is neither null safe nor type safe and we have to explicitly cast the parsed value to the type we want [double].
How can we improve this?
The deep_pick package
The deep_pick package simplifies JSON parsing with a type-safe API.
Once installed, we can use it to get the value we want without manual casts:
deep_pick offers a variety of flexible APIs that we can use to parse primitive types, lists, maps, DateTime objects, and more. Read the documentation for more info.
Bonus: adding a toString[] method
When working with model classes it's very useful to provide a toString[] method so that they can be easily printed to console.
And since we already have a toJson[] method, we can leverage it like so:
As a result, we can print our restaurant directly like this:
It would also be nice if we could compare our model classes using the == operator, as is often required when writing unit tests. To find out how to do this, check out the Equatable package.
Note about performance
When you parse small JSON documents, your application is likely to remain responsive and not experience performance problems.
But parsing very large JSON documents can result in expensive computations that are best done in the background on a separate Dart isolate. The official docs have a good guide about this:
- Parse JSON in the background
sponsor
Build Privacy-First Flutter apps with the @platform. Our Open Source platform, which is built on Dart, gives people control over their own data. Automatically comply with GDPR [and other privacy regulations], earn commissions as you grow, and make apps super fast no backend infrastructure needed.
Conclusion
JSON serialization is a very mundane task. But if we want our apps to work correctly, it's very important that we do it right and pay attention to details:
- use jsonEncode[] and jsonDecode[] from 'dart:convert' to serialize JSON data
- create model classes with fromJson[] and toJson[] for all domain-specific JSON objects in your app
- add explicit casts, validation, and null checks inside fromJson[] to make the parsing code more robust
- for nested JSON data [lists of maps], apply the fromJson[] and toJson[] methods
- consider using the deep_pick package to parse JSON in a type-safe way
While the example JSON we used as reference wasn't too complex, we still ended up with a considerable amount of code:
- Restaurant Ratings example - JSON Serialization code
If you have a lot of different model classes, or each class has a lot of properties, writing all the parsing code by hand becomes time-consuming and error-prone.
In such cases, code generation is a much better option and this article explains how to use it:
- How to Parse JSON in Dart/Flutter with Code Generation using Freezed
And if you need to parse large JSON data, you should do so in a separate isolate for best performance. This article covers all the details:
- How to Parse Large JSON Data with Isolates in Dart 2.15
Happy coding!