How to use generics and list of generics with json serialization in Dart?

16,017

Solution 1

Here's a example about that

https://github.com/dart-lang/json_serializable/blob/master/example/lib/json_converter_example.dart

// json_converter_example.dart


// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:json_annotation/json_annotation.dart';

part 'json_converter_example.g.dart';

@JsonSerializable()
class GenericCollection<T> {
  @JsonKey(name: 'page')
  final int page;

  @JsonKey(name: 'total_results')
  final int totalResults;

  @JsonKey(name: 'total_pages')
  final int totalPages;

  @JsonKey(name: 'results')
  @_Converter()
  final List<T> results;

  GenericCollection(
      {this.page, this.totalResults, this.totalPages, this.results});

  factory GenericCollection.fromJson(Map<String, dynamic> json) =>
      _$GenericCollectionFromJson<T>(json);

  Map<String, dynamic> toJson() => _$GenericCollectionToJson(this);
}

class _Converter<T> implements JsonConverter<T, Object> {
  const _Converter();

  @override
  T fromJson(Object json) {
    if (json is Map<String, dynamic> &&
        json.containsKey('name') &&
        json.containsKey('size')) {
      return CustomResult.fromJson(json) as T;
    }
    if (json is Map<String, dynamic> &&
        json.containsKey('name') &&
        json.containsKey('lastname')) {
      return Person.fromJson(json) as T;
    }
    // This will only work if `json` is a native JSON type:
    //   num, String, bool, null, etc
    // *and* is assignable to `T`.
    return json as T;
  }

  @override
  Object toJson(T object) {
    // This will only work if `object` is a native JSON type:
    //   num, String, bool, null, etc
    // Or if it has a `toJson()` function`.
    return object;
  }
}

@JsonSerializable()
class CustomResult {
  final String name;
  final int size;

  CustomResult(this.name, this.size);

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

  Map<String, dynamic> toJson() => _$CustomResultToJson(this);

  @override
  bool operator ==(Object other) =>
      other is CustomResult && other.name == name && other.size == size;

  @override
  int get hashCode => name.hashCode * 31 ^ size.hashCode;
}

@JsonSerializable()
class Person {
  final String name;
  final String lastname;

  Person(this.name, this.lastname);

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

  Map<String, dynamic> toJson() => _$PersonToJson(this);

  @override
  bool operator ==(Object other) =>
      other is Person && other.name == name && other.lastname == lastname;
}


// main.dart

import './json_converter_example.dart';
import 'dart:convert';

final jsonStringCustom =
    '''{"page":1,"total_results":10,"total_pages":200,"results":[{"name":"Something","size":80},{"name":"Something 2","size":200}]}''';
final jsonStringPerson =
    '''{"page":2,"total_results":2,"total_pages":300,"results":[{"name":"Arya","lastname":"Stark"},{"name":"Night","lastname":"King"}]}''';
void main() {
  // Encode CustomResult
  List<CustomResult> results;
  results = [CustomResult("Mark", 223), CustomResult("Albert", 200)];
  // var customResult = List<CustomResult> data;
  var jsonData = GenericCollection<CustomResult>(
      page: 1, totalPages: 200, totalResults: 10, results: results);
  print({'JsonString', json.encode(jsonData)});

  // Decode CustomResult
  final genericCollectionCustom =
      GenericCollection<CustomResult>.fromJson(json.decode(jsonStringCustom));
  print({'name', genericCollectionCustom.results[0].name});

  // Encode Person

  List<Person> person;
  person = [Person("Arya", "Stark"), Person("Night", "King")];

  var jsonDataPerson = GenericCollection<Person>(
      page: 2, totalPages: 300, totalResults: 2, results: person);
  print({'JsonStringPerson', json.encode(jsonDataPerson)});

  // Decode Person

  final genericCollectionPerson =
      GenericCollection<Person>.fromJson(json.decode(jsonStringPerson));

  print({'name', genericCollectionPerson.results[0].name});
}

the result it's

{JsonStringCustom, {"page":1,"total_results":10,"total_pages":200,"results":[{"name":"Mark","size":223},{"name":"Albert","size":200}]}}
{name, Something}
{JsonStringPerson, {"page":2,"total_results":2,"total_pages":300,"results":[{"name":"Arya","lastname":"Stark"},{"name":"Night","lastname":"King"}]}}
{name, Arya}

Solution 2

here is the my proper solution perfectly worked for me.

class Paginate<T> {
  int from;
  int index;
  int size;
  int count;
  int pages;
  List<T> items;
  bool hasPrevious;
  bool hasNext;

  Paginate(
      {this.index,
      this.size,
      this.count,
      this.from,
      this.hasNext,
      this.hasPrevious,
      this.items,
      this.pages});


  factory  Paginate.fromJson(Map<String,dynamic> json,Function fromJsonModel){
    final items = json['items'].cast<Map<String, dynamic>>();
    return Paginate<T>(
      from: json['from'],
      index: json['index'],
      size: json['size'],
      count: json['count'],
      pages: json['pages'],
      hasPrevious: json['hasPrevious'],
      hasNext: json['hasNext'],
      items: new List<T>.from(items.map((itemsJson) => fromJsonModel(itemsJson)))
    );
  }
}

Lets say we are going to use flight model paginate model. here you must configure the flight list.

class Flight {
  String flightScheduleId;
  String flightId;
  String flightNo;
  String flightDate;
  String flightTime;

  Flight(
      {this.flightScheduleId,
      this.flightId,
      this.flightNo,
      this.flightDate,
      this.flightTime});

  factory Flight.fromJson(Map<String, dynamic> parsedJson) {
    var dateFormatter = new DateFormat(Constants.COMMON_DATE_FORMAT);
    var timeFormatter = new DateFormat(Constants.COMMON_TIME_FORMAT);
    var parsedDate = DateTime.parse(parsedJson['flightDepartureTime']);
    String formattedDate = dateFormatter.format(parsedDate);
    String formattedTime = timeFormatter.format(parsedDate);
    return Flight(
        flightScheduleId: parsedJson['id'],
        flightId: parsedJson['flightLayoutId'],
        flightNo: parsedJson['outboundFlightName'],
        flightDate: formattedDate,
        flightTime: formattedTime,
  }
  // Magic goes here. you can use this function to from json method.
  static Flight fromJsonModel(Map<String, dynamic> json) => Flight.fromJson(json);
}

-> Here you can use,

 Paginate<Flight>.fromJson(responses, Flight.fromJsonModel);

Solution 3

json_serializable

json_serializable has a several strategies1 to handle generic types as single objects T or List<T> (as of v. 5.0.2+) :

  1. Helper Class: JsonConverter
  2. Helper Methods: @JsonKey(fromJson:, toJson:)
  3. Generic Argument Factories @JsonSerializable(genericArgumentFactories: true)

1 Of which I'm aware. There's likely other ways to do this.

Helper Class: JsonConverter

Basic idea: write a custom JsonConverter class with fromJson & toJson methods to identify & handle our Type T field de/serialization.

The nice thing about the JsonCoverter strategy is it encapsulates all your de/serialization logic for your models into a single class that's reusable across any classes needing serialization of the same model types. And your toJson, fromJson calls don't change, as opposed to Generic Argument Factories strategy, where every toJson, fromJson call requires we supply a handler function.

We can use JsonConverter with our object to de/serialize by annotating:

  • individual T / List<T> fields requiring custom handling, or
  • the entire class (where it will be used on any/all fields of type T).

Below is an example of a json_serializable class OperationResult<T> containing a generic type field T.

Notes on OperationResult class:

  • has a single generic type field T t.
  • t can be a single object of type T or a List<T> of objects.
  • whatever type T is, it must have toJson()/fromJson() methods (i.e. be de/serializable).
  • has a JsonConverter class named ModelConverter annotating the T t field.
  • generated stubs _$OperationResultFromJson<T>(json) & _$OperationResultToJson<T>() now take a T variable
/// This method of json_serializable handles generic type arguments / fields by
/// specifying a converter helper class on the generic type field or on the entire class.
/// If the converter is specified on the class itself vs. just a field, any field with
/// type T will be de/serialized using the converter.
/// This strategy also requires us determine the JSON type during deserialization manually,
/// by peeking at the JSON and making assumptions about its class.
@JsonSerializable(explicitToJson: true)
class OperationResult<T> {
  final bool ok;
  final Operation op;
  @ModelConverter()
  final T t;
  final String title;
  final String msg;
  final String error;

  OperationResult({
    this.ok = false,
    this.op = Operation.update,
    required this.t,
    this.title = 'Operation Error',
    this.msg = 'Operation failed to complete',
    this.error= 'Operation could not be decoded for processing'});

  factory OperationResult.fromJson(Map<String,dynamic> json) =>
      _$OperationResultFromJson<T>(json);
  Map<String,dynamic> toJson() => _$OperationResultToJson<T>(this);
}

And here is the JsonConverter class ModelConverter for the above:

/// This JsonConverter class holds the toJson/fromJson logic for generic type
/// fields in our Object that will be de/serialized.
/// This keeps our Object class clean, separating out the converter logic.
///
/// JsonConverter takes two type variables: <T,S>.
///
/// Inside our JsonConverter, T and S are used like so:
///
/// T fromJson(S)
/// S toJson(T)
///
/// T is the concrete class type we're expecting out of fromJson() calls.
/// It's also the concrete type we're inputting for serialization in toJson() calls.
///
/// Most commonly, T will just be T: a variable type passed to JsonConverter in our
/// Object being serialized, e.g. the "T" from OperationResult<T> above.
///
/// S is the JSON type.  Most commonly this would Map<String,dynamic>
/// if we're only de/serializing single objects.  But, if we want to de/serialize
/// Lists, we need to use "Object" instead to handle both a single object OR a List of objects.
class ModelConverter<T> implements JsonConverter<T, Object> {
  const ModelConverter();

  /// fromJson takes Object instead of Map<String,dynamic> so as to handle both
  /// a JSON map or a List of JSON maps.  If List is not used, you could specify
  /// Map<String,dynamic> as the S type variable and use it as
  /// the json argument type for fromJson() & return type of toJson(). 
  /// S can be any Dart supported JSON type
  /// https://pub.dev/packages/json_serializable/versions/6.0.0#supported-types
  /// In this example we only care about Object and List<Object> serialization
  @override
  T fromJson(Object json) {
    /// start by checking if json is just a single JSON map, not a List
    if (json is Map<String,dynamic>) {
      /// now do our custom "inspection" of the JSON map, looking at key names
      /// to figure out the type of T t. The keys in our JSON will
      /// correspond to fields of the object that was serialized.
      if (json.containsKey('items') && json.containsKey('customer')) {
        /// In this case, our JSON contains both an 'items' key/value pair
        /// and a 'customer' key/value pair, which I know only our Order model class
        /// has as fields.  So, this JSON map is an Order object that was serialized
        /// via toJson().  Now I'll deserialize it using Order's fromJson():
        return Order.fromJson(json) as T;
        /// We must cast this "as T" because the return type of the enclosing
        /// fromJson(Object? json) call is "T" and at compile time, we don't know
        /// this is an Order.  Without this seemingly useless cast, a compile time
        /// error will be thrown: we can't return an Order for a method that
        /// returns "T".
      }
      /// Handle all the potential T types with as many if/then checks as needed.
      if (json.containsKey('status') && json.containsKey('menuItem')) {
        return OrderItem.fromJson(json) as T;
      }
      if (json.containsKey('name') && json.containsKey('restaurantId')) {
        return Menu.fromJson(json) as T;
      }
      if (json.containsKey('menuId') && json.containsKey('restaurantId')) {
        return MenuItem.fromJson(json) as T;
      }
    } else if (json is List) { /// here we handle Lists of JSON maps
      if (json.isEmpty) return [] as T;

      /// Inspect the first element of the List of JSON to determine its Type
      Map<String,dynamic> _first = json.first as Map<String,dynamic>;
      bool _isOrderItem = _first.containsKey('status') && _first.containsKey('menuItem');

      if (_isOrderItem) {
        return json.map((_json) => OrderItem.fromJson(_json)).toList() as T;
      }

      bool _isMenuItem = _first.containsKey('menuId') && _first.containsKey('restaurantId');

      if (_isMenuItem) {
        return json.map((_json) => MenuItem.fromJson(_json)).toList() as T;
      }

    }
    /// We didn't recognize this JSON map as one of our model classes, throw an error
    /// so we can add the missing case
    throw ArgumentError.value(json, 'json', 'OperationResult._fromJson cannot handle'
        ' this JSON payload. Please add a handler to _fromJson.');
  }

  /// Since we want to handle both JSON and List of JSON in our toJson(),
  /// our output Type will be Object.
  /// Otherwise, Map<String,dynamic> would be OK as our S type / return type.
  ///
  /// Below, "Serializable" is an abstract class / interface we created to allow
  /// us to check if a concrete class of type T has a "toJson()" method. See
  /// next section further below for the definition of Serializable.
  /// Maybe there's a better way to do this?
  ///
  /// Our JsonConverter uses a type variable of T, rather than "T extends Serializable",
  /// since if T is a List, it won't have a toJson() method and it's not a class
  /// under our control.
  /// Thus, we impose no narrower scope so as to handle both cases: an object that
  /// has a toJson() method, or a List of such objects.
  @override
  Object toJson(T object) {
    /// First we'll check if object is Serializable.
    /// Testing for Serializable type (our custom interface of a class signature
    /// that has a toJson() method) allows us to call toJson() directly on it.
    if (object is Serializable){
      return object.toJson();
    } /// otherwise, check if it's a List & not empty & elements are Serializable
    else if (object is List) {
      if (object.isEmpty) return [];

      if (object.first is Serializable) {
        return object.map((t) => t.toJson()).toList();
      }
    }
    /// It's not a List & it's not Serializable, this is a design issue
    throw ArgumentError.value(object, 'Cannot serialize to JSON',
        'OperationResult._toJson this object or List either is not '
            'Serializable or is unrecognized.');
  }

}

Below is the Serializable interface used for our model classes like Order and MenuItem to implement (see the toJson() code of ModelConverter above to see how/why this is used):

/// Interface for classes to implement and be "is" test-able and "as" cast-able
abstract class Serializable {
  Map<String,dynamic> toJson();
}

Helper Methods: @JsonKey(fromJson:, toJson:)

This annotation is used to specify custom de/serialization handlers for any type of field in a class using json_serializable, not just generic types.

Thus, we can specify custom handlers for our generic type field T t, using the same "peek at keys" logic as we used above in the JsonConverter example.

Below, we've added two static methods to our class OperationResultJsonKey<T> (named this way just for obviousness in this Stackoverflow example):

  • _fromJson
  • _toJson

(These can also live outside the class as top-level functions.)

Then we supply these two methods to JsonKey:

@JsonKey(fromJson: _fromJson, toJson: _toJson)

Then, after re-running our build_runner for flutter or dart (flutter pub run build_runner build or dart run build_runner build), these two static methods will be used by the generated de/serialize methods provided by json_serializable.

/// This method of json_serializable handles generic type arguments / fields by
/// specifying a static or top-level helper method on the field itself.
/// json_serializable will call these hand-typed helpers when de/serializing that particular
/// field.
/// During de/serialization we'll again determine the type manually, by peeking at the
/// JSON keys and making assumptions about its class.
@JsonSerializable(explicitToJson: true)
class OperationResultJsonKey<T> {
  final bool ok;
  final Operation op;
  @JsonKey(fromJson: _fromJson, toJson: _toJson)
  final T t;
  final String title;
  final String msg;
  final String error;


  OperationResultJsonKey({
    this.ok = false,
    this.op = Operation.update,
    required this.t,
    this.title = 'Operation Error',
    this.msg = 'Operation failed to complete',
    this.error = 'Operation could not be decoded for processing'});

  static T _fromJson<T>(Object json) {
    // same logic as JsonConverter example
  }

  static Object _toJson<T>(T object) {
    // same logic as JsonConverter example
  }

  /// These two _$ methods will be created by json_serializable and will call the above
  /// static methods `_fromJson` and `_toJson`.
  factory OperationResultJsonKey.fromJson(Map<String, dynamic> json) =>
      _$OperationResultJsonKeyFromJson(json);

  Map<String, dynamic> toJson() => _$OperationResultJsonKeyToJson(this);

}

Generic Argument Factories @JsonSerializable(genericArgumentFactories: true)

In this final way of specialized handling for de/serialization, we're expected to provide custom de/serialization methods directly to our calls to toJson() and fromJson() on OperationResult.

This strategy is perhaps the most flexible (allowing you to specify exactly how you want serialization handled for each generic type), but it's also very verbose requiring you to provide a serialization handler function on each & every toJson / fromJson call. This gets old really quickly.

toJson

For example, when serializing OperationResult<Order>, the .toJson() call takes a function which tells json_serializable how to serialize the Order field when serializing OperationResult<Order>.

The signature of that helper function would be: Object Function(T) toJsonT

So in OperationResult our toJson() stub method (that json_serializable completes for us) goes from:

Map<String,dynamic> toJson() => _$OperationResultToJson(this);

to:

Map<String,dynamic> toJson(Object Function(T) toJsonT) => _$OperationResultToJson<T>(this, toJsonT);

  • toJson() goes from taking zero arguments, to taking a function as an argument
  • that function will be called by json_serializable when serializing Order
  • that function returns Object instead of Map<String,dynamic> so that it can also handle multiple T objects in a List such as List<OrderItem>

fromJson

For the fromJson() side of genericArgumentFactories used on our OperationResult<Order> class expects us to provide a function of signature: T Function(Object?) fromJsonT

So if our object with a generic type to de/serialize was OperationResult<Order>, our helper function for fromJson() would be: static Order fromJsonModel(Object? json) => Order.fromJson(json as Map<String,dynamic>);

Here's an example class named OperationResultGAF using genericArgumentFactories:

@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
class OperationResultGAF<T> {
  final bool ok;
  final Operation op;
  final String title;
  final String msg;
  final T t;
  final String error;


  OperationResultGAF({
    this.ok = false,
    this.op = Operation.update,
    this.title = 'Operation Error',
    this.msg = 'Operation failed to complete',
    required this.t,
    this.error= 'Operation could not be decoded for processing'});

  // Interesting bits here → ----------------------------------- ↓ ↓
  factory OperationResultGAF.fromJson(Map<String,dynamic> json, T Function(Object? json) fromJsonT) =>
      _$OperationResultGAFFromJson<T>(json, fromJsonT);

  // And here → ------------- ↓ ↓
  Map<String,dynamic> toJson(Object Function(T) toJsonT) =>
      _$OperationResultGAFToJson<T>(this, toJsonT);
}

If T were a class named Order, this Order class could hold static helper methods for use with genericArgumentFactories:

@JsonSerializable(explicitToJson: true, includeIfNull: false)
class Order implements Serializable {

  //<snip>

  /// Helper methods for genericArgumentFactories
  static Order fromJsonModel(Object? json) => Order.fromJson(json as Map<String,dynamic>);
  static Map<String, dynamic> toJsonModel(Order order) => order.toJson();

  /// Usual json_serializable stub methods
  factory Order.fromJson(Map<String,dynamic> json) => _$OrderFromJson(json);
  Map<String,dynamic> toJson() => _$OrderToJson(this);

}

Notice that the above helper methods simply call the usual toJson(), fromJson() stub methods generated by json_serializable.

The point of adding such static methods to model classes is to make supplying these helper methods to OperationResultGAF.toJson(), OperationResultGAF.fromJson() less verbose: we provide just their function names instead of the actual function.

e.g. Instead of:

OperationResultGAF<Order>.fromJson(_json, (Object? json) => Order.fromJson(json as Map<String,dynamic>));

we can use:

OperationResultGAF<Order>.fromJson(_json, Order.fromJsonModel);

If T is a List of objects such as List<MenuItem>, then we need helper methods that handle lists.

Here's an example of static helper methods to add to MenuItem class to handle Lists:

  static List<MenuItem> fromJsonModelList(Object? jsonList) {
    if (jsonList == null) return [];
    
    if (jsonList is List) {
      return jsonList.map((json) => MenuItem.fromJson(json)).toList();
    }
    
    // We shouldn't be here
    if (jsonList is Map<String,dynamic>) {
      return [MenuItem.fromJson(jsonList)];
    }

    // We really shouldn't be here
    throw ArgumentError.value(jsonList, 'jsonList', 'fromJsonModelList cannot handle'
        ' this JSON payload. Please add a handler for this input or use the correct '
        'helper method.');
  }

  /// Not at all comprehensive, but you get the idea
  static List<Map<String,dynamic>> toJsonModelList(Object list) {
    if (list is List<MenuItem>) {
      return list.map((item) => item.toJson()).toList();
    }
    return [];
  }

And an example of how these static helper methods could be called in a unit test:

  List<MenuItem> _mListA = [MockData.menuItem1, MockData.menuItem2];

  OperationResultGAF<List<MenuItem>> _orC = OperationResultGAF<List<MenuItem>>(
      op: Operation.delete, t: _mListA);

  /// Use toJsonModelList to produce a List<Map<String,dynamic>>
  var _json = _orC.toJson(MenuItem.toJsonModelList);

  /// Use fromJsonModelList to convert List<Map<String,dynamic>> to List<MenuItem>
  OperationResultGAF<List<MenuItem>> _orD = OperationResultGAF<List<MenuItem>>.fromJson(
      _json, MenuItem.fromJsonModelList);

  expect(_orC.op, _orD.op);
  expect(_orC.t.first.id, _orD.t.first.id);

Solution 4

lets say we have these two similar json with items list of a generic type

{
   "items":[
      {
         "animalName" : "cat",
         "eyeColor" : "green"
      },
      {
         "personName" : "dog",
         "eyeColor" : "black"
      }  ]
}
{
   "items":[
      {
         "productId" : 123,
         "productName" : "Car"
      },
      {
         "productId" : 567,
         "productName" : "Pencile"
      }
   ]
}

and here is the MAGIC 👇

class ItemsNetwork<T> {
  late List<T> _items;

  List<T> get items => _items;

  T Function(Map<String, dynamic>) itemFromJson;

  ItemsNetwork({
    required this.itemFromJson,
  });

  ItemsNetwork<T> fromJson(Map<String, dynamic> json) {
    _items = (json['items'] as List<dynamic>)
        .map((e) => itemFromJson.call(e as Map<String, dynamic>))
        .toList();
    return this;
  }
}

then you could use it like below:


List<Animal> animals = ItemsNetwork(itemFromJson: Animal.fromJson).fromJson(jsonMap).items;


List<Animal> products = ItemsNetwork(itemFromJson: Product.fromJson).fromJson(jsonMap).items;

Solution 5

If you're using JsonSerializable and build_runner, you could let your models extend from an abstract class with a method that calls _$xxxFromJson(Map<String, dynamic> json) in the JsonSerializable generated code like below.

abstract class FromJsonModel<T> {
  T fromJson(Map<String, dynamic> json);
  static Type typeOf<T>() => T;
}

@JsonSerializable()
class Shop extends FromJsonModel<Shop>{
  String name;

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

  @override
  Shop fromJson(Map<String, dynamic> json) => _$ShopFromJson(json);
}

@JsonSerializable()
class Product extends FromJsonModel<Product>{
  String name;

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

  @override
  Product fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}

And when you connect to a REST endpoint, use a factory method like below and call your model's fromJson like so.

class MyClient {
  Future<T> get<T extends FromJsonModel<T>>(Uri url,
      {Map<String, String>? headers}) async {
    final response =
        await http.get(url, headers: headers).timeout(Duration(seconds: 5));
    dynamic jsonResult = jsonDecode(response.body);
    FromJsonModel model =
        FromJsonModelFactory.get(FromJsonModel.typeOf<T>().toString());
    return model.fromJson(jsonResult);
  }

  Future<List<T>> getList<T extends FromJsonModel<T>>(Uri url,
      {Map<String, String>? headers}) async {
    final response =
        await http.get(url, headers: headers).timeout(Duration(seconds: 5));
    dynamic jsonResult = jsonDecode(response.body);

    if (jsonResult is Iterable) {
      FromJsonModel model = FromJsonModelFactory.get(FromJsonModel.typeOf<T>().toString());
      return jsonResult.map((e) => model.fromJson(e) as T).toList();
    }

    return [];
  }
}

class FromJsonModelFactory {

  static Map<String, FromJsonModel> _processorMap = {
    '$Product': Product(),
    '$Shop': Shop(),
  };

  static FromJsonModel get(String type) {
    if (_processorMap[type] == null) {
      throw new Exception("FromJsonModelFactory: $type is not a valid FromJsonModel type!");
    }

    return _processorMap[type]!;
  }
}

And finally call the client's get / getList methods.

class ProductService {
  late MyClient myClient;

  ProductService() {
    myClient = new MyClient();
  }

  Future<List<Product>> findRecommendedByLocation(Location location, int pageNo) async {
    return myClient.getList(Uri.parse("${URLs.productRecommendedByLocation}/${location}/$pageNo"), headers: HttpSettings.headers);
  }

  Future<Product> findById(String productId) async {
    return myClient.get(Uri.parse("${URLs.product}/$productId"), headers: HttpSettings.headers);
  }
}

As you create new models, you'll have to modify the FromJsonModelFactory but if using dart:mirrors is not an option, this works pretty well actually.

Hopefully someone will find this useful.

Share:
16,017
Marco Coelho
Author by

Marco Coelho

Updated on June 15, 2022

Comments

  • Marco Coelho
    Marco Coelho almost 2 years

    I am developing a mobile project made with Flutter. This project need to connect to some servers for REST consumption services (GET, POST, PUT, DELETE, ...), and retrieve data as well as send data to them. The data needs to be formatted in JSON , so I decided to utilize the Json serialization library 2.0.3 for Dart with Json annotation 2.0.0 and build_runner 1.2.8; It does work just fine for basic data types like int, String and bool, as well as custom objects. But it doesn't seem to work at all for generics, like a <T> item; field for instance or a List<T> list; field.

    My intention is to add some generic fields so they can be used to return all kind of json types and structures. I managed to find a solution for the first case, by using "@JsonKey" to override fromJson and toJson, and comparing <T> with the desired type I wanted to cast it to in the method. However, I couldn't find a solution to List<T> type fields. If I try to use annotation for them, all I get is a List<dynamic> type which is useless to compare classes for casting. How do I solve my predicament? Should I stick to json_serialization or use build_value instead? Any help on this matter would be very much appreciated.

    My code:

    import 'package:json_annotation/json_annotation.dart';
    
    part 'json_generic.g.dart';
    
    @JsonSerializable()
    class JsonGeneric<T> {
      final int id;
      final String uri;
      final bool active;
      @JsonKey(fromJson: _fromGenericJson, toJson: _toGenericJson)
      final T item;
      @JsonKey(fromJson: _fromGenericJsonList, toJson: _toGenericJsonList)
      final List<T> list;
    
      static const String _exceptionMessage = "Incompatible type used in JsonEnvelop";
    
      JsonGeneric({this.id, this.uri, this.active, this.item, this.list});
    
      factory JsonGeneric.fromJson(Map<String, dynamic> json) =>
          _$JsonGenericFromJson(json);
    
      Map<String, dynamic> toJson() => _$JsonGenericToJson(this);
    
      static T _fromGenericJson<T>(Map<String, dynamic> json) {
        if (T == User) {
          return json == null ? null : User.fromJson(json) as T;
        } else if (T == Company) {
          return json == null ? null : Company.fromJson(json) as T;
        } else if (T == Data) {
          return json == null ? null : Data.fromJson(json) as T;
        } else {
          throw Exception(_exceptionMessage);
        }
      }
    
      static Map<String, dynamic> _toGenericJson<T>(T value) {
        if (T == User) {
          return (T as User).toJson();
        } else if(T == Company) {
          return (T as Company).toJson();
        } else if(T == Data) {
          return (T as Data).toJson();
        } else {
          throw Exception(_exceptionMessage);
        }
      }
    
      static dynamic _fromGenericJsonList<T>(List<dynamic> json) {
        if (T == User) {
    
        } else if(T == Company) {
    
        } else if(T == Data) {
    
        } else {
          throw Exception(_exceptionMessage);
        }
      }
    
      static List<Map<String, dynamic>> _toGenericJsonList<T>(dynamic value) {
        if (T == User) {
    
        } else if(T == Company) {
    
        } else if(T == Data) {
    
        } else {
          throw Exception(_exceptionMessage);
        }
      }
    }
    

    I expected to be able to serialize/deserialize "final List list;" either with "@JsonKey" or without it, but so far, I failed to find a way to cast it into the proper json format.

    When I try to generate code for this class (with the command "flutter packages pub run build_runner build"), I end up receiving the following error:

    Error running JsonSerializableGenerator Could not generate fromJson code for list because of type T. None of the provided TypeHelper instances support the defined type. package:json_generic.dart:11:17

       ╷
    11 │   final List<T> list;
       │                 ^^^^
       ╵
    
  • softmarshmallow
    softmarshmallow about 4 years
    not quite the cleanest way.. that violates the concept of generic<T>. any data class with serialization should be acceptable
  • S. Aziz Kazdal
    S. Aziz Kazdal over 3 years
    could check this question please ? stackoverflow.com/questions/63017293/…
  • Nicat
    Nicat almost 3 years
    Thank you. You saved my day
  • Justin
    Justin about 2 years
    Thanks for your very thorough answer. It's worth noting that your toJson method doesn't support encoding primitive types (it throws the Cannot serialize to JSON exception), which is most likely what the generic type property is if it didn't implement Serializable. It might be better to just end the toJson method with return jsonEncode(object); (which will work for primitive types), and wrap that in a try/catch and throw your unsupported exception if that fails. The fromJson method has the same problem.