diff --git a/_includes/sidebar.html b/_includes/sidebar.html index 72fe2a3cf99..4a3d71785ea 100644 --- a/_includes/sidebar.html +++ b/_includes/sidebar.html @@ -38,6 +38,7 @@
  • Platform-specific code
  • Read and write files
  • Network and HTTP
  • +
  • JSON and serialization
  • Firebase for Flutter - Codelab
  • diff --git a/faq.md b/faq.md index b63299edf83..96a4e195c0f 100644 --- a/faq.md +++ b/faq.md @@ -524,6 +524,9 @@ Absolutely. There are libraries in [pub.dartlang.org](https://pub.dartlang.org) for JSON, XML, protobufs, and many other utilities and formats. +For a detailed writeup on using JSON with Flutter, check out [the JSON +tutorial](/json/). + ### Can I build 3D (OpenGL) apps with Flutter? Today we don't support for 3D via OpenGL ES or similar. We have long-term plans diff --git a/images/json/ide_warning.png b/images/json/ide_warning.png new file mode 100644 index 00000000000..b7361ac92de Binary files /dev/null and b/images/json/ide_warning.png differ diff --git a/json.md b/json.md new file mode 100644 index 00000000000..24ced0ef6a1 --- /dev/null +++ b/json.md @@ -0,0 +1,364 @@ +--- +layout: page +title: JSON and serialization +permalink: /json/ +--- + +It is hard to think of a mobile app that doesn't need to communicate with a web +server or easily store structured data at some point. When making +network-connected apps, the chances are that we need to consume some good old +JSON, sooner or later. + +In this tutorial, we look into ways of using JSON with Flutter. We go over what +JSON solution to use in different scenarios and why. + +* TOC Placeholder +{:toc} + +## Which JSON serialization method is right for me? + +This article covers two general strategies for working with JSON: + +* Manual serialization and deserialization +* Automated serialization and deserialization via code generation + +Different projects come with different complexities and use cases. For smaller +proof-of-concept projects or quick prototypes, using code generators might be +overkill. For apps with several JSON models with more complexity, doing +serialization by hand can quickly become tedious, repetitive and lend itself to +lots of small errors. + +### Use manual serialization for smaller projects + +Manual JSON serialization refers to using the built-in JSON decoder in +`dart:convert`. It involves passing the raw JSON string to the `JSON.decode()` +method, and then looking up the values you need in the `Map` +the method returns. It has no external dependencies or particular setup process, +and it is good for quick proof of concepts. + +Where the manual serialization does not perform well is when your project +becomes bigger. Writing the serialization logic by hand can become hard to +manage and error-prone. If you have a typo when accessing an unexisting JSON +field, your code throws an error during runtime. + +If you do not have many JSON models in your project and are looking to test a +concept quickly, manual serialization might be the way you want to start. For an +example of manual serialization, [see here](#manual-serialization). + +### Use code generation for medium to large projects + +JSON serialization with code generation means having an external library +generate the serialization boilerplate for you. They involve some initial setup +and running a file watcher that generates the code from your model classes. For +example, +[json_serializable](https://pub.dartlang.org/packages/json_serializable) and +[built_value](https://pub.dartlang.org/packages/built_value) are these kinds of +libraries. + +This approach scales well for a larger project. There is no hand-written +boilerplate needed, and typos when accessing JSON fields are caught at +compile-time. The downside with code generation is that it involves some initial +setup. Also, the generated source files may produce visual clutter in your +project navigator + +You might want to use generated code for JSON serialization when you have a +medium or a larger project. To see an example of code generation based JSON +serialization, [see here](#code-generation). + +## Is there a GSON/Jackson/Moshi equivalent in Flutter? + +The simple answer is no. + +Such a library would require using runtime reflection, which is disabled in +Flutter. Dart has supported _tree shaking_ for quite a long time. With tree +shaking, we can “shake off” unused code from our release builds. Tree shaking +allows us to optimize the size of our applications significantly. + +Since reflection makes all code implicitly used by default, it interferes with +tree shaking. The tools cannot know what parts are unused at runtime; the +redundant code is impossible to strip away. App sizes cannot be optimized when +using reflection. + + + +Although we cannot use runtime reflection with Flutter, some libraries give us +similarly easy to use APIs but are based on code generation instead. This +approach is covered in more detail in the [code generation +libraries](#code-generation) section. + + +## Serializing JSON manually using dart:convert + +Basic JSON serialization in Flutter is very simple. Flutter has a built-in +`dart:convert` library, which includes a straightforward JSON encoder and +decoder. + +Here is an example JSON for a simple user model. + +```json +{ + "name": "John Smith", + "email": "john@example.com" +} +``` + +With `dart:convert`, we can serialize this JSON model in two ways. Let's have a +look at both. + +### Serializing JSON inline + +By looking at [the dart:convert JSON +documentation](https://api.dartlang.org/stable/1.24.3/dart-convert/JsonCodec-class.html), +we see that we can decode the JSON by calling the `JSON.decode` method, with our +JSON string as the method argument. + + +```dart +Map user = JSON.decode(json); + +print('Howdy, ${user['name']}!'); +print('We sent the verification link to ${user['email']}.'); +``` + +Unfortunately, `JSON.decode()` merely returns a `Map`, meaning +that we do not know the types of the values until runtime. With this approach, +we lose most of the statically typed language features: type safety, +autocompletion and most importantly, compile-time exceptions. Our code can +become instantly more error-prone. + +For example, whenever we access the `name` or `email` fields, we could quickly +introduce a typo. A typo which our compiler does not know of since our entire +JSON merely lives in a map structure. + +### Serializing JSON inside model classes + +We can combat the previously mentioned problems by introducing a plain model +class, which we call `User`. Inside the `User` class, we have: + +* a `User.fromJson` constructor, for constructing a new `User` instance from a + map structure +* a `toJson` method, which converts a `User` instance into a map. + +This way, the _calling code_ can now have type safety, autocompletion for the +`name` and `email` fields and compile-time exceptions. If we make typos or treat +the fields as `int`s instead of `String`s, our app will not even compile, +instead of crashing on runtime. + +**user.dart** + + +```dart +class User { + final String name; + final String email; + + User(this.name, this.email); + + User.fromJson(Map json) + : name = json['name'], + email = json['email']; + + Map toJson() => + { + 'name': name, + 'email': email, + }; +} +``` + +Now the responsibility of the serialization logic is moved inside the model +itself. With this new approach, we can deserialize a user quite easily. + + +```dart +Map userMap = JSON.decode(json); +var user = new User.fromJson(userMap); + +print('Howdy, ${user.name}!'); +print('We sent the verification link to ${user.email}.'); +``` + +To serialize a user, we just pass the `User` object to the `JSON.encode` method. +We don't need to call the `toJson` method here, since `JSON.encode` already does +it for us. + + +```dart +String json = JSON.encode(user); +``` + +This way, the calling code does not have to worry about JSON serialization at +all. However, the model class still definitely has to. In a production app, we +would want to be sure that the serialization works properly. In practice, the +`User.fromJson` and `User.toJson` methods both need to have unit tests in place +to verify correct behavior. + +Also, real-world scenarios are not usually that simple. It is unlikely that we +can get by with such small JSON responses. Nested JSON objects are not that +uncommon either. + +It would be nice if there were something that handled the JSON serialization for +us. Luckily, there is! + + +## Serializing JSON using code generation libraries + +Although there are other libraries available, in this tutorial, we use the +[json_serializable package](https://pub.dartlang.org/packages/json_serializable). It +is an automated source code generator that can generate the JSON serialization +boilerplate for us. + +Since the serialization code is not handwritten and maintained by us anymore, we +minimize the risk of having JSON serialization exceptions at runtime. + +### Setting up json_serializable in a project + +To include `json_serializable` in our project, we need one regular and two _dev +dependencies_. In short, _dev dependencies_ are dependencies that are not +included in our app source code. + +The latest versions of these required dependencies can be seen by following +[this link](https://github.com/dart-lang/json_serializable/blob/master/example/pubspec.yaml). + +**pubspec.yaml** + +```yaml +dependencies: + # Your other regular dependencies here + json_annotation: ^0.2.2 + +dev_dependencies: + # Your other dev_dependencies here + build_runner: ^0.6.1 + json_serializable: ^0.3.0 +``` + +Run `flutter packages get` inside your project root folder (or click "Packages +Get" in your editor) to make these new dependencies available in your project. + +### Creating model classes the json_serializable way + +Let's see how to convert our `User` class to a `json_serializable` one. For the +sake of simplicity, we use the dumbed-down JSON model from the previous samples. + +**user.dart** + + +{% prettify dart %} +/// This allows the generated code access our class members. +/// The value for this is the same as the source file name, +/// in this case, user.dart without the .dart file extension. +library [[highlight]]user[[/highlight]]; + +import 'package:json_annotation/json_annotation.dart'; + +/// This allows our `User` class to access private members in +/// the generated file. The value for this is *.g.dart, where +/// the star denotes the source file name. +part '[[highlight]]user[[/highlight]].g.dart'; + +/// An annotation for the code generator to know that this class needs the +/// JSON serialization logic to be generated. +[[highlight]]@JsonSerializable()[[/highlight]] + +/// Every json_serializable class must have the serializer mixin. +/// It makes the generated toJson() method to be usable for the class. +/// The mixin's name follows the source class, in this case, User. +class User extends Object with _$[[highlight]]User[[/highlight]]SerializerMixin { + User(this.name, this.email); + + String name; + String email; + + /// A necessary factory constructor for creating a new User instance + /// from a map. We pass the map to the generated _$UserFromJson constructor. + /// The constructor is named after the source class, in this case User. + factory User.fromJson(Map json) => _$[[highlight]]User[[/highlight]]FromJson(json); +} +{% endprettify %} + +With this setup, the source code generator will generate code for serializing +the `name` and `email` fields from JSON and back. + +If needed, it is also easy to customize the naming strategy. For example, if the +API we are working with returns objects with _snake\_case_, and we want to use +_lowerCamelCase_ in our models, we can use the `@JsonKey` annotation with a name +parameter: + + +```dart +/// Tell json_serializable that "registration_date_millis" should be +/// mapped to this property. +@JsonKey(name: 'registration_date_millis') +final int registrationDateMillis; +``` + +### Running the code generation utility + +When creating `json_serializable` classes the first time, you will get errors +similar to the image below. + +![IDE warning when the generated code for a model class does not exist +yet.](/images/json/ide_warning.png) + +These errors are entirely normal and are simply because the generated code for +the model class does not exist yet. To resolve this, we must run the code +generator that generates the serialization boilerplate for us. + +There are two ways of running the code generator. + +#### One-time code generation + +By running `flutter packages pub run build_runner build` in our project root, we +can generate json serialization code for our models whenever needed. This +triggers a one-time build which goes through our source files, picks the +relevant ones and generates the necessary serialization code for them. + +While this is pretty convenient, it would nice if we did not have to run the +build manually every time we make changes in our model classes. + +#### Generating code continuously + +A _watcher_ can make our source code generation progress more convenient. It +watches changes in our project files and automatically builds the necessary +files when needed. We can start the watcher by running `flutter packages pub run +build_runner watch` in our project root. + +It is safe to start the watcher once and leave it running in the background. + +### Consuming json_serializable models + +To deserialize a JSON string `json_serializable` way, we do not have actually to +make any changes to our previous code. + + +```dart +Map userMap = JSON.decode(json); +var user = new User.fromJson(userMap); +``` + +Same goes for serialization. The calling API is the same as before. + + +```dart +String json = JSON.encode(user); +``` + +With `json_serializable`, we can forget any manual JSON serialization in the +`User` class. The source code generator creates a file called `user.g.dart`, +which has all the necessary serialization logic. Now we do not necessarily have +to write automated tests to be sure that the serialization works - it is now +_the library's responsibility_ to make sure the serialization works +appropriately. + +## Further references + +* [JsonCodec documentation](https://api.dartlang.org/stable/1.24.3/dart-convert/JsonCodec-class.html) +* [The json_serializable package in Pub](https://pub.dartlang.org/packages/json_serializable) +* [json_serializable examples in GitHub](https://github.com/dart-lang/json_serializable/blob/master/example/lib/example.dart) +* [Discussion about dart:mirrors](https://github.com/flutter/flutter/issues/1150) \ No newline at end of file