Generics and JSON serialization in Flutter

Wamae Benson
3 min readNov 28, 2020

--

Let's say we had the following JSON coming from a server after signing in:

Base Response

{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
},
"message": null
}

We have to create a class that will help us with JSON deserialization.

We could go with the manual way of mapping our object, but there’s a better way.

We will be using the json_serializable plugin found here:

https://pub.dev/packages/json_serializable/install

We will build the following Dart class

@JsonSerializable()
class LoginResponse {
@JsonKey(name: 'access_token')
final String accessToken;
LoginResponse(this.accessToken);factory LoginResponse.fromJson(Map<String, dynamic> json) =>
_$LoginResponseFromJson(json);
Map<String, dynamic> toJson() => _$LoginResponseToJson(this);
}

Simple, right?

Let us add another dimension. What if the “data” could be morphed into other custom data types? It could be any type of object; Enter the world of Generics — what would the class look like?

This is how we would transform our base response class.

import 'package:json_annotation/json_annotation.dart';part 'base_response.g.dart';@JsonSerializable(genericArgumentFactories: true, fieldRename: FieldRename.snake, nullable: true)
class BaseResponse<T> {
@JsonKey(name: 'success’)
final bool success;
@JsonKey(name: 'data’)
final T? data;
@JsonKey(name: 'message’)
final String? message;
BaseResponse(this.success, this.data, this.message);factory BaseResponse.fromJson(
Map<String, dynamic> json,
T Function(Object json) fromJsonT,
) =>
_$BaseResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$BaseResponseToJson(this, toJsonT);
}

After installing our plugin we run the following command:

flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs

This will generate the base_response.g.dart class.// @dart=2.12

part of ‘base_response.dart’;// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LocalBaseResponse<T> _$LocalBaseResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) {
return LocalBaseResponse<T>(
json[‘success’] as bool,
_$nullableGenericFromJson(json[‘data’], fromJsonT),
json[‘message’] as String?,
);
}
Map<String, dynamic> _$LocalBaseResponseToJson<T>(
LocalBaseResponse<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
‘success’: instance.successful,
‘data’: _$nullableGenericToJson(instance.data, toJsonT),
‘message’: instance.message,
};
T? _$nullableGenericFromJson<T>(
Object? input,
T Function(Object? json) fromJson,
) =>
input == null ? null : fromJson(input);
Object? _$nullableGenericToJson<T>(
T? input,
Object? Function(T value) toJson,
) =>
input == null ? null : toJson(input);

Now we are ready to use this in our repository.

Here is an example of how our auth repository would look like.

import 'dart:convert';import 'package:k2_botton_nav/services/web_api/base_response.dart';
import 'package:k2_botton_nav/services/web_api/login_response.dart';
import 'package:http/http.dart' as http;
import 'package:k2_botton_nav/services/web_api/web_api.dart';
import 'package:k2_botton_nav/services/web_api/app_config.dart';
import 'package:k2_botton_nav/services/web_api/login_request.dart';
class AuthRepository implements WebApi {
Future<BaseResponse<LoginResponse?>> signIn(LoginRequest loginRequest) async {
print("AuthRepository.signIn " + "${AppConfig.BASE_URL}/sign_in");
print("AuthRepository.signIn LoginRequest" +
loginRequest.toJson().toString());
http.Response response = await http
.post("${AppConfig.BASE_URL}/sign_in",
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8'
},
body: jsonEncode(loginRequest))
.catchError((resp) {});
if (response == null) {
print("No Internet");
return new BaseResponse(false, null, "No internet");
}
if (response.statusCode < 200 || response.statusCode >= 300) {
print("Error");
return new BaseResponse(false, null, "An error occurred");
}
BaseResponse<LoginResponse?> result = BaseResponse<LoginResponse?>.fromJson(
jsonDecode(response.body),
(data) => data != null ? LoginResponse.fromJson(data) : null);
print(response.body);
return result;
}
}

What if data is nullable?

BaseResponse<LoginResponse?> result = BaseResponse<LoginResponse?>.fromJson(
jsonDecode(response.body),
(data) => data != null ? LoginResponse.fromJson(data) : null);

The line shown above also checks if the data is null before parsing it — it’s that simple.

Question: What does this mean for you as a Flutter dev?

Answer: You can have a consistent way to consume data from your API and data can even be a list since we are now using generics.

--

--