DEV Community

loading...

Dart (Flutter) serialize nested generics

edezacas profile image Edu Deza ・4 min read

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.

Discussion (2)

pic
Editor guide
Collapse
funder7 profile image
Federico Ricchiuto

Awesome! Thank you

Collapse
sanathe06 profile image
Sanath Nandasiri

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