Convert complex Objects to Json || Serialize Objects - json

I need to locally serialize an Object. What is the best way to do this?
I thought the best solution would be to convert my object into a json, the encode this json to a string and serialize this string with shared preferences.
The problem is, there is no toJson() method (as far as I know), that converts the whole object to a json. So I need to create this method myself. However my Object is quite complex with nested Objects and List of Objects and some attributes can be null at times. So a toJson() method would me very complex and cumbersome. It might look something like this just even more complex:
Workout class I want to convert to json:
class Workout {
String id;
String name;
Routine routine;
List<Exercise> exercises;
Map<String, dynamic> toJson() => {
'name': name,
'exercises': [
exercises.map((Exercise exercise) {
print(exercise.toJson());
return exercise.toJson();
}).toString(),
],
};
}
Exercise Class:
class Exercise {
String id;
String presetId;
Timings timings;
String progression;
Preset preset;
Map<String, dynamic> toJson() => {
id != null ? 'id': id : '',
'presetId': presetId,
progression != null ? 'progression': progression : '',
timings == null ? '' : 'timings': timings.toJson(),
};
}
Timings Class:
class Timings {
int holdDown;
int positives;
int hold;
int negatives;
Map<String, dynamic> toJson() => {
'holdDown': holdDown,
'positives': positives,
'hold': hold,
'negatives': negatives
};
}
And the Object might even get bigger and I still need to create functions fromJson(). Also the conditional satements don't work well because there seems to be a conflict between ":" of the statement ":" and the actual json...
Any ideas?

Related

Flutter/Dart: Reduce size of JSON file

I have a a few classes that i serialize nicely with the flutter jsonEncode/jsonDecode macros:
part 'friend.g.dart';
#JsonSerializable(explicitToJson: true)
class Friend {
Friend({required this.name});
#JsonKey(required: true)
late String name;
#JsonKey(required: true)
late final Preferences prefTree;
#JsonKey(required: false, defaultValue: "assets/avatars/cat.png")
late String avatarAsset = avatarAssets[Random().nextInt(avatarAssets.length-1)];
factory Friend.fromJson(Map<String, dynamic> json) => _$FriendFromJson(json);
Map<String, dynamic> toJson() => _$FriendToJson(this);
Map<String, dynamic> _$FriendToJson(Friend instance) => <String, dynamic>{
'name': instance.name,
'avatarAsset': instance.avatarAsset,
'isFavorite': instance.isFavorite,
'prefTree': instance.prefTree,
};
....
}
It works very finely, and when deserializing it will use the default values if the JSON value is not present in the json file.
The problem is the serializer.
What i would like is that
when my class attribute has a default value, and
the attribute is not required, and
the value of the attribute of my instance is the same as the default value,
==> then the serializer would not write down the value in the json file.
This would save me dozens of thousands of lines in the JSON file.
I read about the different JSON members, like "required", "defaultValue", etc, and i use them, but still the serializer does not seem to take this into account.
Again, the deserializer works like a charm.
So instead of having my classes serialized to this:
{
"itemName": "Brown",
"itemIconString": "",
"isAPreference": false
},
I'd like to have it serialized like this (because of the default values):
{
"itemName": "Brown",
},
Is it me or it is not possible to avoid the default value of a member to be outputed in the jsonEncode?
Thanks!
I don't think JsonSerializable provides such an option. but you can write this manually.
here is an example:
class Test {
final int a; // default is 1
final int b; // default is 2
Test({ this.a = 1, this.b = 2});
Map<String, dynamic> toJson() {
final result = <String, dynamic>{};
if (a != 1) {
result['a'] = a;
}
if (b != 2) {
result['b'] = b;
}
return result;
}
}
void main() {
final t = Test(a: 1, b: 1);
print(t.toJson());
}

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

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.

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

Extracting an internal JSON object from another JSON object

I'm trying to show a list of tweets using Twitter API, using the fromJson factory.
Each tweet object has an extended_entities object which is an array of media objects.
If you're not familiar with the Twitter API you can see all the different objects here.
Here are the models i created in order to achieve this:
class Tweet {
final String createdAt;
final int id;
final String idStr;
final String text;
final String inReplyToStatusIdStr;
final String inReplyToUserIdStr;
final TweetExtendedEntities tweetExtendedEntities;
Tweet(
{this.createdAt,
this.id,
this.idStr,
this.text,
this.inReplyToStatusIdStr,
this.inReplyToUserIdStr,
this.tweetExtendedEntities});
factory Tweet.fromJson(Map<String, dynamic> json) {
return new Tweet(
createdAt: json['created_at'] as String,
id: json['id'] as int,
idStr: json['id_str'] as String,
text: json['text'] as String,
inReplyToStatusIdStr: json['in_reply_to_status_id_str'] as String,
inReplyToUserIdStr: json['in_reply_to_user_id_str'] as String,
tweetExtendedEntities: json['extended_entities'] as TweetExtendedEntities,
);
}
}
class TweetExtendedEntities {
final List<TweetMedia> tweetMedia;
TweetExtendedEntities({this.tweetMedia});
factory TweetExtendedEntities.fromJson(Map<String, dynamic> json) {
return new TweetExtendedEntities(
tweetMedia: json['media'] as List<TweetMedia>);
}
}
class TweetMedia {
final String mediaType;
final String mediaUrl;
TweetMedia({this.mediaType, this.mediaUrl});
factory TweetMedia.fromJson(Map<String, dynamic> json) {
return new TweetMedia(
mediaType: json['type'] as String,
mediaUrl: json['media_url'] as String,
);
}
}
Before i tried to get the extended_entities object everything was fine and i successfully got the JSON data and parsed it, but when i try to get the media objects using the code above, i get this error:
I/flutter (29538): type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'TweetExtendedEntities' in type cast where
I/flutter (29538): _InternalLinkedHashMap is from dart:collection
I/flutter (29538): String is from dart:core
I/flutter (29538): TweetExtendedEntities is from package:pubg_companion/models/tweet.dart
How can i get nested JSON objects using factory or any other way?
Dart has no idea that your JSON structure will correspond to your object, or how. You can't directly cast the JSON (which is probably a Map<String, dynamic>, but could also be other things) to your objects. #betorcs answer is a start in the right direction but needs a bit more.
This line:
tweetExtendedEntities: json['extended_entities'] as TweetExtendedEntities,
Needs to be
tweetExtendedEntities: TweetExtendedEntities.fromJson['extended_entities'],
And your TweetExtendedEntities method should look more like this:
factory TweetExtendedEntities.fromJson(Map<String, dynamic> json) {
return new TweetExtendedEntities(
tweetMedia: createTweetMediaList(json['media']));
}
static List<TweetMedia> createTweetMediaList(List json) {
if (json == null) return null;
if (json.isEmpty) return [];
return json.map((tweetMediaJson) => TweetMedia.fromJson(tweetMediaJson)).toList();
}
You could also certainly look into json_serializable if your needs start getting more complicated and you want to try to generate some of this code.
Your json parameter is Map<String, dynamic>, dynamic is not TweetExtendedEntities, but it can be cast to Map.
factory Tweet.fromJson(Map<String, dynamic> json) {
return new Tweet(
createdAt: json['created_at'] as String,
id: json['id'] as int,
idStr: json['id_str'] as String,
text: json['text'] as String,
inReplyToStatusIdStr: json['in_reply_to_status_id_str'] as String,
inReplyToUserIdStr: json['in_reply_to_user_id_str'] as String,
tweetExtendedEntities: TweetExtendedEntities.fromJson(json['extended_entities'] as Map),
);
}
In flutter, this error will be thrown when you pass a string without json.decode() to the fromjson factory
eg:
Map bodyJson = json.decode(loginResponse.body);
var login = new LoginResponse.fromJson(bodyJson);
The LoginResponse class
class LoginResponse {
int responseCode;
String message;
String responseObject;
LoginResponse();
factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
}
JSON and serialization in Flutter