Working with JSON #
Manual serialization #
Decoding #
In Dart we can deserialize JSON using
jsonDecode
function from the dart:convert
library.
The function has a return type of dynamic
, which means that Dart can’t tell
what type it is before running the application.
It is important to know how types a treated when working with JSON. Here are some examples you can play around with:
import 'dart:convert';
final json = '''{
"id": 35,
"categories": ["Programming","Geeky"],
"joke": "There are only 10 kinds of people in this world: those who know binary and those who don't.",
"flags": {
"explicit": false
}
}''';
void main() {
final data1 = jsonDecode(json);
print(data1.toString());
print("\nRuntime type: ${data1.runtimeType}");
print("Can I use it as a Map? ${data1 is Map}");
print("Can I use it as a Map<String, dynamic>? ${data1 is Map<String, dynamic>}");
print("Can I use it as a Map<String, Object>? ${data1 is Map<String, Object>}");
print("\n");
final categories = data1['categories'];
print('The field "categories": $categories');
print('Runtime type: ${categories.runtimeType}');
print('Is it a List<dynamic>? ${categories is List<dynamic>}');
print('Is it a List<String>? ${categories is List<String>}');
print('Is first element a String? ${categories[0] is String}');
}
Encoding #
For serialization to JSON we can use jsonEncode.
It works fine for the following types:
- num, int, double
- bool
- String
- List
- Map
import 'dart:convert';
void main() {
final data = {
"id": 35,
"categories": ["Programming","Geeky"],
"joke": "There are only 10 kinds of people in this world: those who know binary and those who don't.",
"flags": {
"explicit": false
}
};
final json = jsonEncode(data);
print(json);
}
Notice how similar Dart literals are to JSON.
You can make the JSON more readable to humans with
JsonEncoder.withIndent('\t').convert
.
import 'dart:convert';
void main() {
final data = { "id": 35, "categories": ["Programming","Geeky"], "joke": "There are only 10 kinds of people in this world: those who know binary and those who don't.", "flags": { "explicit": false } };
final json = JsonEncoder.withIndent('\t').convert(data);
print(json);
}
Data transfer objects #
What if we want to work with classes? We don’t get classes back when deserializing. And we can not serialize classes directly.
Attempting to do so, gives us a nasty error.
import 'dart:convert';
class JokeDto {
int? id;
List<String>? categories;
String? setup;
String? delivery;
JokeDto({this.id, this.categories, this.setup, this.delivery});
}
void main() {
final joke = JokeDto(
id: 1,
categories: ["Programming"],
setup: ".NET developers are picky when it comes to food.",
delivery: "They only like chicken NuGet.",
);
jsonEncode(joke);
}
Instead, we need some methods to convert between Map<String, dynamic>
and our
DTO.
It is common to put those conversion methods in the DTO class.
import 'dart:convert';
const json = '''{
"id": 49,
"categories": [
"Programming"
],
"setup": ".NET developers are picky when it comes to food.",
"delivery": "They only like chicken NuGet."
}''';
class JokeDto {
int? id;
List<String>? categories;
String? setup;
String? delivery;
JokeDto({this.id, this.categories, this.setup, this.delivery});
JokeDto.fromJson(Map<String, dynamic> json) {
id = json['id'];
categories = json['categories']?.cast<String>();
setup = json['setup'];
delivery = json['delivery'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['categories'] = this.categories;
data['setup'] = this.setup;
data['delivery'] = this.delivery;
return data;
}
}
void main() {
final dto = JokeDto.fromJson(jsonDecode(json));
print(dto);
print(jsonEncode(dto.toJson()));
}
Wondering why the methods are called fromJson
and toJson
when they work with Map
type?
It is just a convention that people in the Dart community use.
The convention implies that fromJson
is compatible with jsonDecode
and
toJson
with jsonEncode
.
Code generation #
That’s a lot of code to write just to support JSON serialization for a class. This is an area where Dart falls short a bit (in my opinion). It’s a well known problem within Dart/Flutter community, so several solutions exist in the form of code generation.
Code generation is tooling that can generate code for you. You define a simple class and have the tool generate all the boilerplate code to support JSON generation for you. Here is where the ecosystem gets a bit fragmented as several packages exist that solved the problem of JSON serialization. However, they all use build_runner under the hood to generate the code.
Here are some options:
The package json_serializable
only gives you JSON serialization.
It is often used in combination with the
equatable that you’ve seen previously.
The other options do the same as the json_serializable
+ equatable
combo,
but also provides additional helper methods for working with immutable classes.
Tho not the most popular option, I think dart_mappable
is perhaps the easiest
to grasp, so that is what we will go with.
Many examples online use a combination of
freezed
andflutter_bloc
. It might be more popular simply because it has existed for longer.
dart_mappable #
To use dart_mappable
we need to add a couple of dependencies, as described in the package docs.
flutter pub add dart_mappable
flutter pub add build_runner --dev
flutter pub add dart_mappable_builder --dev
Say you have a file named joke_dto.dart
.
Now, instead of this:
class JokeDto extends Equatable {
String? setup;
String? delivery;
int? id;
JokeDto({this.setup, this.delivery, this.id});
JokeDto.fromJson(Map<String, dynamic> json) {
setup = json["setup"];
delivery = json["delivery"];
id = json["id"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> _data = <String, dynamic>{};
_data["setup"] = setup;
_data["delivery"] = delivery;
_data["id"] = id;
return _data;
}
JokeDto copyWith({
String? setup,
String? delivery,
int? id,
}) => JokeDto(
setup: setup ?? this.setup,
delivery: delivery ?? this.delivery,
id: id ?? this.id,
);
@override
List<Object> get props => [setup, delivery, id];
}
You can write this:
part 'joke_dto.mapper.dart';
@MappableClass()
class JokeDto with JokeDtoMappable {
String? setup;
String? delivery;
int? id;
JokeDto({this.setup, this.delivery, this.id });
}
The catch it that you need to run the following command each time you change the class:
dart pub run build_runner build
If your mappable class is defined in a file called
your_class.dart
then you need to putpart 'your_class.mapper.dart';
at the top of the file. It won’t work without it.
Check out the package page for more information.