DEV Community

Edu Deza
Edu Deza

Posted on

Dart (Flutter) serialize nested generics

Few days ago, we’ve had to serialize a paged results response.

This PagedResult object has a List of Generic results, so depending on what result we want to receive, we need to map as one or othe Class.

The Trouble

Flutter doesn’t supports Reflection and neither allow to pass class or constructor as function parameter.

Model examples

class PagedResult<T> {
  final int totalItems;
  final int startItems;
  final int itemsPerPage;
  final int currentPage;
  final int totalPages;
  final List<T> results;

  PagedResult({
  this.totalItems,
  this.startItems,
  this.itemsPerPage,
  this.currentPage,
  this.totalPages,
  this.results});
}

Enter fullscreen mode Exit fullscreen mode

@JsonSerializable()
class Customer {
  final int id;
  final String firstName;
  final String lastName;
  final String secondLastName;
  final String phone1;
  final String phone2;
  final String email;

  Customer(
      {this.id,
      this.firstName,
      this.lastName,
      this.secondLastName,
      this.phone1,
      this.phone2,
      this.email});

 factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);

  Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

@JsonSerializable()
class Product {
  final int id;
  final String ref;
  final double price;
  final String description;
  final double tax;

  Product(
      {this.id,
      this.ref,
      this.price,
      this.description,
      this.tax});

 factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);

  Map<String, dynamic> toJson() => _$ProductToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

Our goal is to serialize PagedResults as PagedResults of Customer and PagedResults of Product.

We have multiple options to do it:

Brute force

We can create one PagedResult class for each "class":


class PagedResultCustomer<Customer> {
  final int totalItems;
  final int startItems;
  final int itemsPerPage;
  final int currentPage;
  final int totalPages;
  final List<Customer> results;

  PagedResult({
  this.totalItems,
  this.startItems,
  this.itemsPerPage,
  this.currentPage,
  this.totalPages,
  this.results});

 factory PagedResultCustomer.fromJson(Map<String, dynamic> json) {
   final items = json['results'].cast<Map<String, dynamic>>();
   return PagedResult<Customer>(
    totalItems = json.totalItems;
     .
     .
     .
    results = new List<Customer>.from(items.map((itemsJson) => Customer.fromJson(itemsJson)))
    )
 }
}

class PagedResultProduct<Product> {
  final int totalItems;
  final int startItems;
  final int itemsPerPage;
  final int currentPage;
  final int totalPages;
  final List<Product> results;

  PagedResult({
  this.totalItems,
  this.startItems,
  this.itemsPerPage,
  this.currentPage,
  this.totalPages,
  this.results});

factory PagedResultProduct.fromJson(Map<String, dynamic> json) {
  final items = json['results'].cast<Map<String, dynamic>>();  
  return PagedResult<Product>(
    totalItems = json.totalItems;
     .
     .
     .
    results = new List<Product>.from(items.map((itemsJson) => Product.fromJson(itemsJson)))
    )
}

Enter fullscreen mode Exit fullscreen mode

It's a simple solution but not accomplish with DRY PRINCIPLE

Young Padawan

Another way is try to follow DRY PRINCIPLE and based on code above we can do:


class PagedResult<T> {
  final int totalItems;
  final int startItems;
  final int itemsPerPage;
  final int currentPage;
  final int totalPages;
  final List<T> results;

  PagedResult({
  this.totalItems,
  this.startItems,
  this.itemsPerPage,
  this.currentPage,
  this.totalPages,
  this.results});

 /// We require a second parameter to be an object that implements a 
/// fromJson method. Ok, I'm not using Typing for this, but if object
/// hasn't fromJson method, we'll throw a new Exception :)
factory PagedResult.fromJson(Map<String, dynamic> json, object) {
  final items = json['results'].cast<Map<String, dynamic>>(); 
  return PagedResult<T>(
    totalItems = json.totalItems;
     .
     .
     .
    results = new List<T>.from(items.map((itemsJson) => object.fromJson(itemsJson)))
    )
 }
}

@JsonSerializable()
class Customer {
  final int id;
  final String firstName;
  final String lastName;
  final String secondLastName;
  final String phone1;
  final String phone2;
  final String email;

  Customer(
      {this.id,
      this.firstName,
      this.lastName,
      this.secondLastName,
      this.phone1,
      this.phone2,
      this.email});

/// We don't use factory, instead we implement a fromJson method object
Customer fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);

 Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

To run:

/// We pass a new object Customer as parameter
final PagedResult<Customer> pagedCustomer = PagedResult<Customer>.fromJson(json, new Customer());

Enter fullscreen mode Exit fullscreen mode

Ok, it's a better solution than Brute Force but I don't like the requirement to need to instantiate a new object for this.

Jedi

Dart doesn't support Reflection, or pass class/factory as parameter, but it allow to pass a method :)

So we can change a bit the code above:


class PagedResult<T> {
  final int totalItems;
  final int startItems;
  final int itemsPerPage;
  final int currentPage;
  final int totalPages;
  final List<T> results;

  PagedResult({
  this.totalItems,
  this.startItems,
  this.itemsPerPage,
  this.currentPage,
  this.totalPages,
  this.results});

 /// We require a second parameter to be a Function, and we'll trigger 
/// this function for serializing results property
 factory PagedResult.fromJson(Map<String, dynamic> json, Function fromJson) {
   final items = json['results'].cast<Map<String, dynamic>>();
   return PagedResult<T>(
    totalItems = json.totalItems;
     .
     .
     .
    results = new List<T>.from(items.map((itemsJson) => fromJson(itemsJson)))
    )
 }
}

@JsonSerializable()
class Customer {
  final int id;
  final String firstName;
  final String lastName;
  final String secondLastName;
  final String phone1;
  final String phone2;
  final String email;

  Customer(
      {this.id,
      this.firstName,
      this.lastName,
      this.secondLastName,
      this.phone1,
      this.phone2,
      this.email});

/// We change object method to a static class method (avoid instantiate object)
static Customer fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);

 Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

To run:

/// We pass a new object Customer as parameter
final PagedResult<Customer> pagedCustomer = PagedResult<Customer>.fromJson(json, Customer.fromJson);

Enter fullscreen mode Exit fullscreen mode

Of course, there are more solutions to serialize generics but I think the Jedi method is a fine and clear solution to do it.

Top comments (5)

Collapse
 
rmpt profile image
Rui Teixeira

You are the man! I'm figthing this freaking generic thingy for an entire day, glad I found your solution. I was very close, the function parameter did the trick, can't go with a 100% clean solution, seems the Jedi is the way to go with Dart. Thanks!

Collapse
 
sanathe06 profile image
Sanath Nandasiri

It seems Jedi method is best so far, you save my day man, thank you very much

Collapse
 
benz738 profile image
Federico R.

Awesome! Thank you

Collapse
 
onuryont profile image
Onur Yönt

What about nested generics ? If there is a base response class that wraps PagedResult like

class BaseResponse {
bool succeeded;
String message;
PagedResult innerData;
}

Collapse
 
edezacas profile image
Edu Deza

You can do something like this:

class BaseResponse<T> {
bool succeeded;
String message;
PagedResult<T> innerData;

BaseResponse({this.succeeded, this.message, this.innerData});

 factory BaseResponse.fromJson(Map<String, dynamic> json, Function fromJson) {
   return BaseResponse<T>(
    succeeded: json.succeeded,
     message: json.message,
    innerData: PagedResult<T>.fromJson(json.pager, fromJson)
    );
 }
}
Enter fullscreen mode Exit fullscreen mode

An as the last example with Customer:

final BaseResponse<Customer> baseResp = BaseResponse<Customer>.fromJson(json, Customer.fromJson);
Enter fullscreen mode Exit fullscreen mode

Of course the "json" structure depends on your http response.