Dynamic List in Flutter for Json - 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.

Related

parsing JSON with a nested, dynamic type

Let's assume I have 3 data classes (which in reality may be many more). Class A or class B shall be wrapped in class Wrap with a data type identifier. This shall allow using the same process inside the app for any data type provided by the json interface.
class Wrap{
int id;
List<dynamic> dataSet;
Wrap({required this.id, required this.dataSet});
}
class A{
int a=0;
A({this.a});
}
class B{
bool b=true;
B({this.b});
}
My aim was to use a map getDataType = {0: A, 1: B}; and then to parse the nested Json with
dataSets: List<dynamic>.from(json["dataSets"].map((d) => getDataType[json["id"].fromJson(d)).toList();
But unfortunately this already fails when trying to access a class method from the variable containing the type
class A{
int i=0; A({i});
factory A.test(int i) => A(2*i);
}
main() {
final t = A;
final x = t.test(2);
}
Anyone with a smart idea how to achieve this?
The rational behind this unorthodox data structure is: I have a data manager in the business logic layer that triggers different UIs depending on some events, coming from a different logic. The manager then fetches the specific data for the UI. To streamline this, the same repository with the same functions shall be used. However, the repository function uses a different database table depending on the UI.
UPDATE: Most basic example json:
{
"id": 0,
"data": [
{
"problemId": 0,
"level": 2
}
]
}
First of all create an abstact class for datasets
abstract class Dataset {
const Dataset();
factory Dataset.fromMap(Map<String, dynamic> map, int type) {
switch (type) {
case 0:
return ADataset.fromMap(map);
case 1:
return BDataset.fromMap(map);
default:
throw Exception("Class with id $type couldn't be found");
}
}
}
and replace List<dynamic> dataSet; in Wrap class with List<Dataset> dataSet;
Now you can simply create dataset classes by extending this Dataset class
class ADataset extends Dataset {
final int fieldA;
ADataset(
this.fieldA,
);
factory ADataset.fromMap(Map<String, dynamic> map) {
return ADataset(map['int field']);
}
}
class BDataset extends Dataset {
final String fieldB;
BDataset(
this.fieldB,
);
factory BDataset.fromMap(Map<String, dynamic> map) {
return BDataset(map['string field']);
}
}
So now all you need to do is
dataSets: List<Dataset>.from(map['datasets'].map(
(dataset) => Dataset.fromMap(dataset, map['type']),
));

Flutter serialize from json depending on type

I want to convert a nested model to the correct model depending on the type. I know that #JsonKey can be used to specifically handle certain properties. However, how can I access other properties on fromJson? The fromJson method has to be static, but then I cannot access the other properties. Does anyone have any idea how to solve this?
#JsonSerializable(explicitToJson: true, nullable: true)
class Model {
int type;
#JsonKey(
name: 'action',
fromJson: _convertActionToModel,
)
dynamic action;
Model({this.type, this.action});
factory Model.fromJson(Map<String, dynamic> json) =>
_$ModelFromJson(json);
Map<String, dynamic> toJson() => _$ModelToJson(this);
static dynamic _convertActionToModel(dynamic json) {
switch (type) { // How can i get this type?
case 0:
return OtherModel.fromJson(json as Map<String, dynamic>);
break;
....
}
}
How can I get the type for the switch case?
Your json is just a map<String, Object>, you could do:
var type = json['typeKey'] as String (or int, bool);

dart/Flutter: Problems with Decodeing json with utf8 decode

I try to load a json-file to put it in a filterable/searchable Listview (search for a diagnosis with a symptom). I'm new to in programming so probably there is a better / simpler way to do this but i would like to do it this way, so it doesnt get more complicated.
I get this error if i try to use utf8.decode:
"The argument type 'String' can't be assigned to the parameter type 'List'."
This is what i tried:
class Services {
static Future<List<User>> getUsers() async {
final response = await rootBundle.loadString('assets/diff.json');
List<User> list = parseUsers(response);
return list;
}
static List<User> parseUsers(String responseBody) {
final parsed = json.decode(utf8.decode(responseBody)).cast<Map<String, dynamic>>();
return parsed.map<User>((json) => User.fromJson(json)).toList();
}
}
the User Class:
class User {
String symptom;
String diagnosis;
User(this.symptom, this.diagnosis);
User.fromJson(Map<String, dynamic> json){
symptom = json['symptom'];
diagnosis = json['diagnosis'];
}
}
extract of the json file:
[
{"symptom":"Kopfschmerz","diagnosis":"Migräne, Spannungskopfschmerz"}
,
{"symptom":"Bauchschmerz","diagnosis":"Apendizitis, Infektion"}
]
Is there a simple way to make this work? Thanks!
With dynamic json.decode(String) the returned object can have a real type of:
List<dynamic>
Map<String, dynamic>
But also when the type is List<dynamic>, decode has parsed also the items in the List, so in your case (since your json has structure [{"" : ""}]) you just need to cast the (reified) List's type parameter with the cast() method.
static List<User> parseUsers(String responseBody) {
//final parsed = json.decode(utf8.decode(responseBody)).cast<Map<String, dynamic>>();
final parsed = (json.decode(responseBody) as List<dynamic>).cast<Map<String, dynamic>>();
return parsed.map<User>((json) => User.fromJson(json)).toList();
}

Flutter parsing json with DateTime from Golang RFC3339: FormatException: Invalid date format

When trying to read json files generated with golangs json package in Dart / Flutter I noticed that parsing dates produce an error:
FormatException: Invalid date format
An example is the following json generated on the Go server:
{
...
"dateCreated": "2018-09-29T19:51:57.4139787-07:00",
...
}
I am using the code generation approach for json (de-)serialization to avoid writing all the boiler plate code. The json_serializable package is a standard package available for this purpose. So my code looks like the following:
#JsonSerializable()
class MyObj {
DateTime dateCreated;
MyObj( this.dateCreated);
factory MyObj.fromJson(Map<String, dynamic> json) => _$MyObjFromJson(json);
Map<String, dynamic> toJson() => _$MyObjToJson(this);
}
Because the documentation doesn't cover this sufficiently it took me a day of researching the flutter sources and trial & error of different things to solve it. So may as well share it.
Golang by default encodes the time.Time in RFC3339 when serializing to Json (like in the given example). Flutter explicitly supports RFC3339, so why doesn't it work? The answer is a small difference in how the seconds fraction part is supported. While Golang produces a precision of 7 digits Dart only supports up to 6 digits and does not gracefully handle violations. So if the example is corrected to only have 6 digits of precision it will parse just fine in Dart:
{
...
"dateCreated": "2018-09-29T19:51:57.413978-07:00",
...
}
In order to solve this in a generic way you have two options: 1. to truncate the additional precision from the string, or 2. implement your own parsing. Let's assume we extend the DateTime class and create your own CustomDateTime. The new class has the parse method overridden to remove all excess after 6 digits before handing it to the parent class' parse method.
Now we can use the CustomDateTime in our Dart classes. For example:
#JsonSerializable()
class MyObj {
CustomDateTime dateCreated;
MyObj( this.dateCreated);
factory MyObj.fromJson(Map<String, dynamic> json) => _$MyObjFromJson(json);
Map<String, dynamic> toJson() => _$MyObjToJson(this);
}
But of course now the code generation is broken and we get the following error:
Error running JsonSerializableGenerator
Could not generate 'toJson' code for 'dateCreated'.
None of the provided 'TypeHelper' instances support the defined type.
Luckily the json_annotation package now has an easy solution for us - The JsonConverter. Here is how to use it in our example:
First define a converter that explains to the code generator how to convert our CustomDateTime type:
class CustomDateTimeConverter implements JsonConverter<CustomDateTime, String> {
const CustomDateTimeConverter();
#override
CustomDateTime fromJson(String json) =>
json == null ? null : CustomDateTime.parse(json);
#override
String toJson(CustomDateTime object) => object.toIso8601String();
}
Second we just annotate this converter to every class that is using our CustomDateTime data type:
#JsonSerializable()
#CustomDateTimeConverter()
class MyObj {
CustomDateTime dateCreated;
MyObj( this.dateCreated);
factory MyObj.fromJson(Map<String, dynamic> json) => _$MyObjFromJson(json);
Map<String, dynamic> toJson() => _$MyObjToJson(this);
}
This satisfies the code generator and Voila! We can read json with RFC3339 timestamps that come from golang time.Time.
I havd the same problem. I found a very simple solution. We can use a custom converter with JsonConverter. For more explanation you can use my article.
import 'package:json_annotation/json_annotation.dart';
class CustomDateTimeConverter implements JsonConverter<DateTime, String> {
const CustomDateTimeConverter();
#override
DateTime fromJson(String json) {
if (json.contains(".")) {
json = json.substring(0, json.length - 1);
}
return DateTime.parse(json);
}
#override
String toJson(DateTime json) => json.toIso8601String();
}
import 'package:json_annotation/json_annotation.dart';
import 'package:my_app/shared/helpers/custom_datetime.dart';
part 'publication_document.g.dart';
#JsonSerializable()
#CustomDateTimeConverter()
class PublicationDocument {
final int id;
final int publicationId;
final DateTime publicationDate;
final DateTime createTime;
final DateTime updateTime;
final bool isFree;
PublicationDocument({
this.id,
this.publicationId,
this.publicationDate,
this.createTime,
this.updateTime,
this.isFree,
});
factory PublicationDocument.fromJson(Map<String, dynamic> json) =>
_$PublicationDocumentFromJson(json);
Map<String, dynamic> toJson() => _$PublicationDocumentToJson(this);
}

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

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)