Flutter - How to parsed nested json to a class with generics? - json

I'm wondering how can I parse a nested json to a class with generic types. My intention is to wrap responses from the backend (like loginRespose that contains a token) with a code and a message
I have
class BaseResponse<T>{
int code;
String message;
T responseObject;
BaseResponse.fromJson(Map<String, dynamic> parsedJson)
: code = parsedJson['Code'],
message = parsedJson['Message'],
responseObject = T.fromJson(parsedJson['ResponseObject']); //This is what I'd like to do
}
Obviously the last line throws an error because T doesn't has a named constructor "fromJson".
I tried adding some restrictions to the Type but I didn't find any solutions. Do you have any idea on how to pull this off?

You can't do such thing, at least not in flutter. As dart:mirror is disabled and there's no interface for classes constructors.
You'll have to take a different route.
I'll recommend using POO instead. You would here give up on deserializing responseObject from your BaseResponse. And then have subclass of BaseResponse handles this deserialization
Typically you'd have one subclass per type:
class IntResponse extends BaseResponse<int> {
IntResponse.fromJson(Map<String, dynamic> json) : super._fromJson(json) {
this.responseObject = int.parse(json["Hello"]);
}
}
You can then hide this mess away by adding a custom factory constructor on BaseResponse to make it more convenient to use.
class BaseResponse<T> {
int code;
String message;
T responseObject;
BaseResponse._fromJson(Map<String, dynamic> parsedJson)
: code = parsedJson['Code'],
message = parsedJson['Message'];
factory BaseResponse.fromJson(Map<String, dynamic> json) {
if (T == int) {
return IntResponse.fromJson(json) as BaseResponse<T>;
}
throw UnimplementedError();
}
}
Then either instantiate the wanted type directly, or use the factory constructor :
final BaseResponse foo = BaseResponse.fromJson<int>({"Hello": "42", "Code": 42, "Message": "World"});

You can achieve this with the built_value package (you'll also need built_value_generator and build_runner). Your class will look something like this:
part 'base_response.g.dart';
abstract class BaseResponse<T> implements Built<BaseResponse<T>, BaseResponseBuilder<T>> {
int code;
String message;
T responseObject;
factory BaseResponse([updates(BaseResponseBuilder<T> b)]) = _$BaseResponse<T>;
static Serializer<BaseResponse> get serializer => _$baseResponseSerializer;
}
You will have to run flutter packages pub run build_runner build to make the generated file. Then you use it like this:
BaseResponse baseResponse = serializers.deserialize(
json.decode(response.body),
specifiedType: const FullType(BaseResponse, const [FullType(ConcreteTypeGoesHere)])
);
There's just one more bit of boilerplate you have to take care of. You need another file called serializers.dart. You need to manually add all the classes you want to deserialize here, and also an addBuilderFactory function for each class that takes a type parameter - and for each concrete type you want to use.
part 'serializers.g.dart';
#SerializersFor(const [
BaseResponse,
ConcreteTypeGoesHere,
])
final Serializers serializers = (_$serializers.toBuilder()
..addBuilderFactory(
FullType(BaseResponse, const [const FullType(ConcreteTypeGoesHere)]),
() => new BaseResponseBuilder<ConcreteTypeGoesHere>()
)
..addPlugin(StandardJsonPlugin()))
.build();
Then re-run flutter packages pub run build_runner build
Makes me wish for Gson... :S

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)

Related

Dart json serializable - decode value as different type

Let's say I'm building an app that receives a json as a response, for example:
{
"a": 5,
"b": [1, 2, 3]
}
Let's say I want to parse this json into the following class:
class Example {
String a;
List<String> b;
}
Using Dart's json_serializable package I can do the following
String intToString(int value) => value.toString();
List<String> intToStringList(List<int> value) => value.map(intToString).toList();
#JsonSerializable()
class Example {
#JsonKey(fromJson: intToString)
String a;
#JsonKey(fromJson: intToStringList)
List<String> b;
Example(this.a, this.b);
factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
Map<String, dynamic> toJson() => _$ExampleToJson(this);
}
My question is if there is any built in, more elegant way to do this without custom method implementations.
I do not think that the problem is in the implementation of the generator. I tried another generator and there is also no other way.
import 'dart:io';
import 'package:object_serializer/json_serializer_generator.dart';
import 'package:yaml/yaml.dart';
void main() {
final classes = loadYaml(_classes) as Map;
final g = JsonSerializerGenerator();
final classesCode = g.generateClasses(classes);
final values = {
'classes': classesCode,
};
var source = g.render(_template, values);
source = g.format(source);
File('bin/stackoverflow.dart').writeAsStringSync(source);
}
const _classes = r'''
Example:
fields:
a:
type: String
deserialize: "(x){ return x == null ? '0' : '$x'; }"
b:
type: List<String>
deserialize: "(x){ return x == null ? <String>[] : (x as List).map((e) => e == null ? '0' : '$x').toList(); }"
''';
const _template = r'''
{{classes}}
''';
The same problem.
Even a little more code is required, as "deserialize" is not post-processing, but a complete implementation of a custom deserialization processing.
class Example {
Example({required this.a, required this.b});
factory Example.fromJson(Map json) {
return Example(
a: (x) {
return x == null ? '0' : '$x';
}(json['a']),
b: (x) {
return x == null
? <String>[]
: (x as List).map((e) => e == null ? '0' : '$x').toList();
}(json['b']),
);
}
final String a;
final List<String> b;
static List<Example> fromJsonList(List json) {
return json.map((e) => Example.fromJson(e as Map)).toList();
}
Map<String, dynamic> toJson() {
return {
'a': a,
'b': b,
};
}
static List<Map<String, dynamic>> toJsonList(List<Example> list) {
return list.map((e) => e.toJson()).toList();
}
}
So the cleanest way I found of doing this is using a JsonConverter:
class StringListFromIntListConverter implements JsonConverter<List<String>, List<int>> {
const StringListFromIntListConverter();
#override
List<String> fromJson(List<int> json) => json.map((num) => num.toString()).toList()
#override
List<int> toJson(List<String> object) => object.map((str) => int.parse(str)).toList();
}
Then use this converter wherever is needed like this:
#JsonSerializable()
#StringListFromIntListConverter()
class Example {
String a;
List<String> b;
Example(this.a, this.b);
factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
Map<String, dynamic> toJson() => _$ExampleToJson(this);
}
This will automatically deserialize and serialize string lists in this class to and from int lists.
The nice thing is that this can be implemented for every type and reused in different places.
Found this really useful for time and durations objects that are received as some kind of number and I want my members to be the actual type I need.

jsonEncode throwing exceptions encoding a simple class

Friends
I have a simple Dart class that cannot be encoded into JSON.
The output of the following code prints out to the console
flutter: Converting object to an encodable object failed: Instance of 'TestJsonConversion'
class TestJsonConversion {
String testString = "123245abcde";
int testIneger = 1234;
}
void main() {
var testJsonConversion = TestJsonConversion();
try {
var testString = jsonEncode(testJsonConversion);
// ignore: avoid_print
print(testString);
}catch(e){
// ignore: avoid_print
print(e.toString());
}
runApp(const MyApp());
}
This is the default application generated by Visual Studio with just these lines added.
You cannot encode an instance of a user class with the built-in jsonEncode. These are things you can encode by default: "a number, boolean, string, null, list or a map with string keys". For this class to encode, you'd have to define a .toJson method on it, and I don't see one there.
The class has no constructors and tojson . Try this
class TestJsonConversion {
final String testString;
final int testInteger;
TestJsonConversion(this.testString, this.testInteger);
TestJsonConversion.fromJson(Map<String, dynamic> json)
: testString = json['trstString'],
testInteger = json['testInteger'];
Map<String, dynamic> toJson() => {
'testString': testString,
'testInteger': testInteger,
};
}
And when you create an instance
var testJsonConversion = TestJsonConversion(testString: 'abc', testInteger: 123);
print(json.encode(testJsonConversion.toJson());

Encode Map with Enum to JSON

I want to encode my Map to a json. It looks like this:
Map<MyEnum, int> map = {type: limit};
Where MyEnum is an enum. A simple json.encode(map) won't work as it does not know how to serialize the enum class I guess.
The error message is:
Unhandled Exception: Converting object to an encodable object failed: _LinkedHashMap len:1
How can I manage to serialize this map to a json?
you can use describeEnum method inside foundation.dart
This is really not a solution I would recommend but I ended up doing it mostly for "fun". I don't guarantee anything about the solution besides the fact that it is horrible. :)
The problem is that enum is not defined as a valid type for Json so the whole concept does give us some problems. One solution is to translate enum values into String with the name of the enum first, and then the name of value like MyFirstEnum.first1. This representation is what Dart gives you if calling toString() on a enum value.
This is fine but for safety we could also add a magic string in the beginning like DART_ENUM:MyFirstEnum.first1 so it is easier to recognize between other strings which could have the same name as the enum value without being an enum.
Next is type safety. In Json, we know that all maps has String as the type of keys. By making our own representation of enum and allowing it to also be keys, we cannot expect a decoder to return e.g. Map<String, dynamic> but must return Map<dynamic, dynamic>.
With that said, here is my attempt to build a Json decoder and encoder which handles enum values. It also works for enum keys in maps:
import 'dart:convert';
class JsonConverterWithEnumSupport {
final String magicString;
final Set<Object> allEnumValues = {};
final Map<String, Object> enumStringToEnumValue = {};
JsonConverterWithEnumSupport(List<List<Object>>? enumsValues,
{this.magicString = "DART_ENUM:"}) {
enumsValues?.forEach(addEnumValues);
}
void addEnumValues(List<Object> enumValues) {
for (final enumValue in enumValues) {
enumStringToEnumValue[enumValue.toString()] = enumValue;
allEnumValues.add(enumValue);
}
}
String _addMagic(dynamic enumValue) => '$magicString$enumValue';
String _removeMagic(String string) => string.substring(magicString.length);
String encode(Object? value) =>
json.encode(value, toEncodable: (dynamic object) {
if (object is Map) {
return object.map<dynamic, dynamic>((dynamic key, dynamic value) =>
MapEntry<dynamic, dynamic>(
allEnumValues.contains(key) ? _addMagic(key) : key,
allEnumValues.contains(value) ? _addMagic(value) : value));
}
if (object is List) {
return object.map<dynamic>(
(dynamic e) => allEnumValues.contains(e) ? _addMagic(e) : e);
}
if (allEnumValues.contains(object)) {
return _addMagic(object);
}
return object;
});
dynamic decode(String source) => json.decode(source, reviver: (key, value) {
if (value is String && value.startsWith(magicString)) {
return enumStringToEnumValue[_removeMagic(value)];
}
if (value is Map) {
return value.map<dynamic, dynamic>((dynamic key, dynamic value) =>
MapEntry<dynamic, dynamic>(
(key is String) && key.startsWith(magicString)
? enumStringToEnumValue[_removeMagic(key)]
: key,
value));
}
return value;
});
}
enum MyFirstEnum { first1, first2 }
enum MySecondEnum { second1, second2 }
void main() {
final converter =
JsonConverterWithEnumSupport([MyFirstEnum.values, MySecondEnum.values]);
final jsonString = converter.encode({
MyFirstEnum.first1: [MySecondEnum.second2, MySecondEnum.second1],
'test': {MyFirstEnum.first2: 5}
});
print(jsonString);
// {"DART_ENUM:MyFirstEnum.first1":["DART_ENUM:MySecondEnum.second2","DART_ENUM:MySecondEnum.second1"],"test":{"DART_ENUM:MyFirstEnum.first2":5}}
print(converter.decode(jsonString));
// {MyFirstEnum.first1: [MySecondEnum.second2, MySecondEnum.second1], test: {MyFirstEnum.first2: 5}}
}
You will need to feed into JsonConverterWithEnumSupport all the possible enum values there is possible (see the main method in the bottom for example).
If you don't want the magic string to be appended on each enum you can just create JsonConverterWithEnumSupport with: magicString: '' as parameter.
You could create an extension on your enum to convert it to a String then convert your map to a Map<String, int> so it will be encoded correctly:
import 'dart:convert';
enum MyEnum { type }
extension MyEnumModifier on MyEnum {
String get string => this.toString().split('.').last;
}
void main() {
Map<MyEnum, int> map = {MyEnum.type: 10};
Map<String, int> newMap = {};
map.forEach((key, value) =>
newMap[key.string] = value);
final json = jsonEncode(newMap);
print(json);
}
Output
{"type":10}
you will need a function to convert string to enum:
T enumFromString<T>(List<T> values, String value) {
return values.firstWhere((v) => v.toString().split('.')[1] == value, orElse: () => null);
}
your enum is
enum MyEnum {type, destype};
suppose it is used as map inside a class to serialize and deserialize:
class MyClass {
Map<MyEnum, int> myProperty = {type: 1};
// serialize
Map<String, dynamic> toJson() {
return {
'myProperty': myProperty.map((key, value) => MapEntry(key.name, value)),
}
// deserialize
MyClass.fromJson(Map<String, dynamic> json) {
myProperty=
json['myProperty'].map((k, v) => MapEntry(enumFromString<MyEnum>(MyEnum.values, k), v)).cast<MyEnum, int>();
}
}
Firstly define enum and value extension
enum OrderState { pending, filled, triggered, cancelled }
extension OrderStateExt on OrderState {
String get value {
switch (this) {
case OrderState.pending:
return "PENDING";
case OrderState.filled:
return "FILLED";
case OrderState.triggered:
return "TRIGGERED";
case OrderState.cancelled:
return "CANCELLED";
}
}
}
model class
class OrderRequest {
OrderState state;
OrderRequest({required this.state});
Map<String, dynamic> toMap() {
return {
'state': state.value,
};
}
}
String toJson() => json.encode(toMap());
factory OrderRequest.fromMap(Map<String, dynamic> map) {
return OrderRequest (
state: OrderState.values
.where((e) => e.value == map['state']).first
);
}
factory OrderRequest.fromJson(String source) =>
OrderRequest.fromMap(json.decode(source));

Flutter Persistence: how to jsonDecode a List<dynamic> to List<ClassType>?

I have a Todo-List app with Task class.
I want to serialize a Task List with jsonEncode and persist them onto a file in Docs dir.
after that, I want to be able to re-serialize the same list and convert them into my native List datatype (from List<String, dynamic> that I get from jsonDecode). Whats the best way to do it?
Currently, I tried:
void reSerializeTaskList() async {
final directory = await getApplicationDocumentsDirectory();
File f = File('${directory.path}/new.txt');
String fileContent = await f.readAsString();
List<dynamic> jsonList = jsonDecode(fileContent).cast<Task>(); // does not work
print("JSONSTRING: ${jsonList.runtimeType}");
print("$jsonList");
}
I/flutter (29177): JSONSTRING: CastList<dynamic, Task>
E/flutter (29177): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Task' in type cast
my workaround is to iterate through all array elements and build a Task type out of the values with "fromJson" method inside my Task class:
void reSerializeTaskList() async {
final directory = await getApplicationDocumentsDirectory();
File f = File('${directory.path}/new.txt');
String fileContent = await f.readAsString();
List<dynamic> jsonList = jsonDecode(fileContent);
List<Task> taskList = [];
for (var t in jsonList) {
print("T: $t and ${t.runtimeType}");
Task task = new Task();
taskList.add(task.fromJson(t));
}
print("JSONSTRING: ${jsonList.runtimeType}");
print("$jsonList");
print("$taskList");
print("$taskList.runtimeType");
}
my Task class:
import 'dart:io';
class Task {
String name;
bool isDone;
Task({this.name, this.isDone = false});
void toggleDone() {
isDone = !isDone;
}
#override
String toString() {
// TODO: implement toString
return "${this.name} is done: $isDone";
}
Map<String, dynamic> toJson() {
return {
"name": this.name,
"isDone": this.isDone,
};
}
Task fromJson(Map<String, dynamic> json) {
this.name = json['name'];
this.isDone = json['isDone'];
return this;
}
}
But is there maybe another (better) approach? This looks quite patchy to me...
Just to give you a little example, this is how I do it
final jsonResponse = json.decode(jsonString);
final List<Customer> customers = jsonResponse.map<Customer>((jR) => Customer.fromJson(jR)).toList();
and fromJson in Customer class looks like this
factory Customer.fromJson(Map<String, dynamic> json) => Customer(
id: json["id"] == null ? null : json["id"],
changeDate: json["changeDate"] == null ? null : DateTime.parse(json["changeDate"]),
name: json["name"] == null ? null : json["name"],
);

Dynamic List in Flutter for Json

I'm working with some complex json in dart, and I have an issue creating objects before I know what type they'll be.
I appreciate the suggestions, but I don't think I completely understand. In the given answer:
var entity = Model();
castToEntity(entity, {'test': 10});
Don't I need to know that it will be a Model class?
What if I have the below two classes:
#JsonSerializable(explicitToJson: true, includeIfNull: false)
class Location {
String id;
String resourceType;
Location({#required this.id, this.resourceType})
factory Location.fromJson(Map<String, dynamic> json) => _$LocationFromJson(json);
Map<String, dynamic> toJson() => _$LocationToJson(this);
}
class Reference {
String reference;
String resourceType;
Location({#required this.reference, this.resourceType}
factory Reference.fromJson(Map<String, dynamic> json) => _$ReferenceFromJson(json);
Map<String, dynamic> toJson() => _$ReferenceToJson(this);
}
And then I query the server, and I don't know what kind of class it will be. It could be a Location, or a Reference, or if it's a list, it could be multiple of both, and I don't know until I've requested it.
var myBundle = Bundle.fromJson(json.decode(response.body));
Each "myBundle.entry" is another resource. I'd like to be able to use information from that resource to define itself. So I could do something like:
myBundle.entry.resourceType newResource = new myBundle.entry.resourceType();
What I'm doing right now is sending it to a function that has all of the possible options predefined:
var newResource = ResourceTypes(myBundle.entry[i].resource.resourceType,
myBundle.entry[i].resource.toJson());
dynamic ResourceTypes(String resourceType, Map<String, dynamic> json) {
if (resourceType == 'Location') return (new Location.fromJson(json));
if (resourceType == 'Reference') return (new Reference.fromJson(json));
}
It was said that there's not reflection in dart, so I didn't know any other way to do it.
As far as I know, it's not possible since Dart doesn't have Reflection like c#, the most close that I can imagine, is using an abstract class that enforces your entity to implement fromJson, and, in that method, you read the Map and put values into fields, like the code below:
abstract class Serializable {
void fromJson(Map<String,dynamic> data);
}
class Model implements Serializable {
int test;
#override
void fromJson(data) {
test = data['test'];
}
}
Serializable castToEntity(Serializable entity, Map<String, dynamic> data) {
return entity..fromJson(data);
}
Now, when you read you database and have the Map, you can call a method generic like:
var entity = Model();
castToEntity(entity, {'test': 10});
print(entity.test);
Where entity is an empty model.
Note: Your fields on entity, cannot be final, since fromJson is an instance method and not a factory method.