Unlocking Efficiency šŸ’« : Building a Generic API Response Template in Dart with JSON Serializable and Retrofit

Sangini Gupta
3 min readJun 16, 2024

--

Dart Generics

Letā€™s Suppose all your API responses come in the form

{
"message": "Request processed successfully",
"errorCode": "200",
"success": true,
"serverTime": "2024-02-04T12:30:00Z",
"data": {
"result": "Example data",
"details": {
"info1": "Information 1",
"info2": "Information 2"
}
}
}

In scenarios where our API responses consistently encapsulate data within a ā€˜dataā€™ field, deserializing the data requires parsing the overall API response first. While creating separate model classes for API responses and data is a common approach, it can become repetitive and cumbersome for various data types.

To address this, Dartā€™s json_serializable package offers a solution. By utilizing its features, we can design a generic class that encompasses the necessary fields and facilitates seamless deserialization of data, regardless of its type.

The magic of the json_serializable is that it generates additional ā€˜helperā€™ parameters for the fromJson and toJson methods, enabling smooth serialization and deserialization of values associated with those generic types.

Setting the Stage

Dependencies: šŸ”©

Before diving into the code, make sure to include the necessary dependencies in your pubspec.yaml file

dependencies:
json_annotation: ^4.0.1
json_serializable: ^4.5.0
retrofit: ^4.1.0
dev_dependencies:
build_runner: ^2.1.7
retrofit_generator: ^7.0.1

Run flutter pub get to fetch the dependencies.

Code Generation: āš™ļø

Create a new Dart file for the API response template base_response.dart. Weā€™ll leverage code generation, so add the part directive for base_response.g.dart to store the generated code.

Our base response class hence will look like this.

import 'package:json_annotation/json_annotation.dart';

part 'base_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseAPIResponse<T> {
final String? message;
final String? errorCode;
final bool? success;
final int? serverTime;
final T? data;

BaseAPIResponse(
{this.message, this.errorCode, this.success, this.serverTime, this.data});

factory BaseAPIResponse.fromJson(
Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$BaseAPIResponseFromJson(json, fromJsonT);

Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$BaseAPIResponseToJson(this, toJsonT);
}
  1. genericArgumentFactories: true is responsible for supporting serialzation of generic types
  2. The first factory method is responsible for creating an instance of BaseAPIResponse<T> from a JSON map. It takes two parameters:
  • Map<String, dynamic> json: The JSON data received from the API response.
  • T Function(Object? json) fromJsonT: A function that will be used to deserialize the data field of type T. The fromJsonT function is responsible for converting the JSON data into the actual type T.

3. The second method converts an instance of BaseAPIResponse<T> to a JSON map. It takes a single parameter:

  • Object Function(T value) toJsonT: A function responsible for serializing the data field of type T into JSON. The toJsonT function is expected to handle the serialization of the specific type T.

This is the magic of this package as it creates these implementation methods _$BaseAPIResponseFromJson, _$BaseAPIResponseToJson, during code generation.

Now we can run

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

The generated file will look like this which will handle the serialization/deserialization of the generic type.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'base_response.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

BaseAPIResponse<T> _$BaseAPIResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
BaseAPIResponse<T>(
message: json['message'] as String?,
errorCode: json['errorCode'] as String?,
success: json['success'] as bool?,
serverTime: json['serverTime'] as int?,
data: _$nullableGenericFromJson(json['data'], fromJsonT),
);

Map<String, dynamic> _$BaseAPIResponseToJson<T>(
BaseAPIResponse<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'message': instance.message,
'errorCode': instance.errorCode,
'success': instance.success,
'serverTime': instance.serverTime,
'data': _$nullableGenericToJson(instance.data, toJsonT),
};

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);

Notice how easy it got to just wrap our model class with BaseResponse and now we can again run the build runner to generate the api_service.g file

If we look closely we can find a code snippet

final value = BaseAPIResponse<TicketFeedbackModel>.fromJson(
_result.data!,
(json) => TicketFeedbackModel.fromJson(json as Map<String, dynamic>),
);

Retrofit uses the BaseAPIResponse.fromJson and provides the TicketFeedBackModel.fromJson to create Object of type BaseAPIResponse<TicketFeedBackModel>

So cool! Right? šŸ˜²

Now we can directly use

final <BaseAPIResponse<TicketFeedbackModel>> response= 
await sendFeedback(request: request, tId: ticketId);
if(response.success==true){
return response.data?? NullResponseData();
}
else{
throw(Exception([response.message, response.errorCode])
}

To sum up, leveraging Dartā€™s JSON Serializable and Flutter Retrofit enables the creation of a generic API response template. This approach streamlines response handling, promotes code reusability, and enhances overall development efficiency.

Happy Coding! ā­

--

--