简体   繁体   中英

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

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;
   │                 ^^^^
   ╵

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}

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

json_serializable

json_serializable has a several strategies 1 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 (ie 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.

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

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.

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;

Here is my approach:

class Wrapper<T, K> {
  bool? isSuccess;
  T? data;

  Wrapper({
    this.isSuccess,
    this.data,
  });

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

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

Wrapper<T, K> _$WrapperFromJson<T, K>(Map<String, dynamic> json) {
  return Wrapper<T, K>(
    isSuccess: json['isSuccess'] as bool?,
    data: json['data'] == null ? null : Generic.fromJson<T, K>(json['data']),
  );
}

class Generic {
  /// If T is a List, K is the subtype of the list.
  static T fromJson<T, K>(dynamic json) {
    if (json is Iterable) {
      return _fromJsonList<K>(json) as T;
    } else if (T == LoginDetails) {
      return LoginDetails.fromJson(json) as T;
    } else if (T == UserDetails) {
      return UserDetails.fromJson(json) as T;
    } else if (T == Message) {
      return Message.fromJson(json) as T;
    } else if (T == bool || T == String || T == int || T == double) { // primitives
      return json;
  } else {
      throw Exception("Unknown class");
    }
  }

  static List<K> _fromJsonList<K>(List<dynamic> jsonList) {
    return jsonList?.map<K>((dynamic json) => fromJson<K, void>(json))?.toList();
  }
}

In order to add support for a new data model, simply add it to Generic.fromJson:

else if (T == NewDataModel) {
  return NewDataModel.fromJson(json) as T;
}

This works with either generic objects:

Wrapper<Message, void>.fromJson(someJson)

Or lists of generic objects:

Wrapper<List<Message>, Message>.fromJson(someJson)

I'm doing it like this, no need for the hackey and weird 'peek at keys' method. I'm a little surprised to see that method in the package documentation .

Additions to a typical JsonSerializable class are:

  • @_Converter() on line 4
  • _Converter<T> class below the Response<T> class

Here DataModels are also JsonSerializable .

@JsonSerializable()
class Response<T> {
  final int count;
  @_Converter()
  final List<T> results;

  Response(this.count, this.results);

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

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

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

  @override
  T fromJson(Object? json) {
    switch (T) {
      case DataModel1:
        return DataModel1.fromJson(json as Map<String, dynamic>) as T;
      case DataModel2:
        return DataModel2.fromJson(json as Map<String, dynamic>) as T;
      case DataModel3:
        return DataModel3.fromJson(json as Map<String, dynamic>) as T;
      default:
        throw UnsupportedError('Unsupported type: $T');
    }
  }

  @override
  Object? toJson(T object) => object;
}

I tried a to fix this. I avoid using factory pattern. U can checkout the full sourcce code. I used asbraction with generics to convert fromJson.

https://github.com/khayavena/http_delegate.git

One possible solution is to write a small macro and use it. The build ( build_runner ) process takes a few seconds.

The simplest example is to write a macro that will create code that will call the fromJson method on a class, using its type as a parameter.

Example:
Input library (eg. bin/main.dart )

@pragma('meta_expression:build')
library my_cool_library;

import 'dart:convert';

@pragma('meta_expression:import')
import 'package:test_meta_expression/my_cool_macros.dart';

void main(List<String> args) {
  final inp1 = jsonDecode('{"value": "Hello from Foo1"}') as Map;
  final inp2 = jsonDecode('{"value": "Hello from Foo2"}') as Map;
  final out1 = _deserialize(Foo1, inp1);
  if (out1 is Foo1) {
    print(out1.value);
  }
  final out2 = _deserialize(Foo2, inp2);
  if (out2 is Foo2) {
    print(out2.value);
  }
  final Foo1 out3 = _deserialize2(inp1);
  print(out3.value);
  final Foo2 out4 = _deserialize2(inp2);
  print(out4.value);
}

/// The '[_deserialize]' function uses the '[deserializeTypes]' macro declared
/// by you (or someone else). It is executed before compilation (at build time).
dynamic _deserialize(Type type, Map json) => deserializeTypes([
      Foo1,
      Foo2,
    ])(type, json);

/// The '[_deserialize2]' function uses the '[deserializeTypes2]' macro declared
/// by you (or someone else). It is executed before compilation (at build time).
T _deserialize2<T>(Map json) => deserializeTypes2([
      Foo1,
      Foo2,
    ])(json);

class Foo1 {
  final String value;
  Foo1({required this.value});
  static Foo1 fromJson(Map json) {
    return Foo1(value: json['value'] as String);
  }
}

class Foo2 {
  final String value;
  Foo2({required this.value});
  static Foo2 fromJson(Map json) {
    return Foo2(value: json['value'] as String);
  }
}

Output library ( bin/main.impl.dart ).
The code is generated by a macro using build_runner .

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// MetaExpressionLibraryGenerator
// **************************************************************************

library my_cool_library;

import 'dart:convert';

void main(List<String> args) {
  final inp1 = jsonDecode('{"value": "Hello from Foo1"}') as Map;
  final inp2 = jsonDecode('{"value": "Hello from Foo2"}') as Map;
  final out1 = _deserialize(Foo1, inp1);
  if (out1 is Foo1) {
    print(out1.value);
  }
  final out2 = _deserialize(Foo2, inp2);
  if (out2 is Foo2) {
    print(out2.value);
  }
  final Foo1 out3 = _deserialize2(inp1);
  print(out3.value);
  final Foo2 out4 = _deserialize2(inp2);
  print(out4.value);
}

/// The '[_deserialize]' function uses the '[deserializeTypes]' macro declared
/// by you (or someone else). It is executed before compilation (at build time).
dynamic _deserialize(Type type, Map json) => (Type type, Map json) {
      const map = {Foo1: Foo1.fromJson, Foo2: Foo2.fromJson};
      final f = map[type];
      if (f == null) {
        throw ArgumentError.value(type, 'type', 'Unknown type');
      }
      return f(json);
    }(type, json);

/// The '[_deserialize2]' function uses the '[deserializeTypes2]' macro declared
/// by you (or someone else). It is executed before compilation (at build time).
T _deserialize2<T>(Map json) => (Map json) {
      const map = {Foo1: Foo1.fromJson, Foo2: Foo2.fromJson};
      final f = map[T];
      if (f == null) {
        throw ArgumentError.value(T, 'type', 'Unknown type');
      }
      return f(json) as T;
    }(json);

class Foo1 {
  final String value;
  Foo1({required this.value});
  static Foo1 fromJson(Map json) {
    return Foo1(value: json['value'] as String);
  }
}

class Foo2 {
  final String value;
  Foo2({required this.value});
  static Foo2 fromJson(Map json) {
    return Foo2(value: json['value'] as String);
  }
}

Result of work (output):

Hello from Foo1
Hello from Foo2
Hello from Foo1
Hello from Foo2

Thus: bin\main.dart is the input file, bin\main.impl.dart is the output file, Exactly the same, but with macro substitutions.

Below is the code for the simplest macro for this example.
lib/my_cool_macros.dart

import 'package:meta_expression_annotation/ast.dart';
import 'package:meta_expression_annotation/meta_expression_annotation.dart';
import 'package:meta_expression_annotation/render.dart';

@MetaExpression(deserializeTypesImpl)
external dynamic Function(Type type, Map json) deserializeTypes(
    List<Type> types);

String deserializeTypesImpl(MetaContext context) {
  const template = '''
(Type type, Map json) {
  const map = {{entries}};
  final f = map[type];
  if (f == null) {
    throw ArgumentError.value(type, 'type', 'Unknown type');
  }
  return f(json);
}''';
  final typesNode = context.getArgument('types');
  if (typesNode is! ListLiteral) {
    throw ArgumentError.value(
        typesNode.source, 'types', 'Must be ListLiteral');
  }

  final elements = typesNode.elements;
  final idents = elements.whereType<Identifier>().toList();
  if (idents.length != elements.length) {
    throw ArgumentError.value(
        typesNode.source, 'types', 'Must contains only identifiers');
  }

  final entries = idents.map((e) => '$e: $e.fromJson');
  final values = {
    'entries': '{${entries.join(', ')}}',
  };
  return render(template, values);
}

@MetaExpression(deserializeTypes2Impl)
external T Function<T>(Map json) deserializeTypes2(List<Type> types);

String deserializeTypes2Impl(MetaContext context) {
  const template = '''
(Map json) {
  const map = {{entries}};
  final f = map[T];
  if (f == null) {
    throw ArgumentError.value(T, 'type', 'Unknown type');
  }
  return f(json) as T;
}''';
  final typesNode = context.getArgument('types');
  if (typesNode is! ListLiteral) {
    throw ArgumentError.value(
        typesNode.source, 'types', 'Must be ListLiteral');
  }

  final elements = typesNode.elements;
  final idents = elements.whereType<Identifier>().toList();
  if (idents.length != elements.length) {
    throw ArgumentError.value(
        typesNode.source, 'types', 'Must contains only identifiers');
  }

  final entries = idents.map((e) => '$e: $e.fromJson');
  final values = {
    'entries': '{${entries.join(', ')}}',
  };
  return render(template, values);
}

Build command:

dart run build_runner build

File pubspec.yaml

dependencies:
  meta_expression_annotation: 0.2.1
dev_dependencies:
  build_runner: any
  meta_expression: 0.2.1

PS

Completely forgot. Example with List<T> .

@pragma('meta_expression:build')
library my_cool_library;

import 'dart:convert';

@pragma('meta_expression:import')
import 'package:test_meta_expression/my_cool_macros.dart';

void main(List<String> args) {
  //
  final jsonList = jsonDecode(
      '[{"value": "Hello from Foo1"}, {"value": "Goodbay from Foo1"}]') as List;
  final List<Foo1> list = _deserializeList(jsonList.cast());
  list.forEach((e) => print(e.value));
}

List<T> _deserializeList<T>(List<Map> jsonList) =>
    jsonList.map((e) => _deserialize2<T>(e)).toList();

T _deserialize2<T>(Map json) => deserializeTypes2([
      Foo1,
      Foo2,
    ])(json);

class Foo1 {
  final String value;
  Foo1({required this.value});
  static Foo1 fromJson(Map json) {
    return Foo1(value: json['value'] as String);
  }
}

class Foo2 {
  final String value;
  Foo2({required this.value});
  static Foo2 fromJson(Map json) {
    return Foo2(value: json['value'] as String);
  }
}

Output:

Hello from Foo1
Goodbay from Foo1

Are there any questions?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM