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 has a several strategies1 to handle generic types as single objects T
or List<T>
(as of v. 5.0.2+) :
JsonConverter
@JsonKey(fromJson:, toJson:)
@JsonSerializable(genericArgumentFactories: true)
1Of which I'm aware. There's likely other ways to do this.
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:
T
/ List<T>
fields requiring custom handling, orT
).Below is an example of a json_serializable class OperationResult<T>
containing a generic type field T
.
Notes on OperationResult
class:
T t
.t
can be a single object of type T
or a List<T>
of objects.T
is, it must have toJson()/fromJson()
methods (i.e. be de/serializable).JsonConverter
class named ModelConverter
annotating the T t
field._$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();
}
@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);
}
@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.
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 argumentOrder
Object
instead of Map<String,dynamic>
so that it can also handle multiple T
objects in a List
such as List<OrderItem>
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With