Background: I would like to create an abstract JsonSerializable class that allows its extenders to have an toJsonString-method and a fromJsonString constructor, so that I can bundle all calls to jsonDecode and jsonEncode in that one class, while also enforcing that the extenders implement the required toJson-metrhod and fromJson-constructor.
Thus far, I have managed to do half of that like this:
json_serializable.dart
import 'dart:convert';
abstract class JsonSerializable {
Map<String, dynamic> toJson();
String toJsonString() => jsonEncode(toJson());
}
readable_uuid.dart
import 'dart:convert';
import 'package:uuid/uuid.dart';
import 'json_serializable.dart';
class ReadableUuid extends JsonSerializable {
static const Uuid uuidGenerator = Uuid();
String uuid = uuidGenerator.v4();
static const String _uuidFieldName = "uuid";
ReadableUuid();
ReadableUuid.fromJson(Map<String, dynamic> json)
: uuid = json[_uuidFieldName];
ReadableUuid.fromJsonString(String jsonString)
: this.fromJson(jsonDecode(jsonString));
#override
Map<String, dynamic> toJson() => {
_uuidFieldName: uuid,
};
[...]
}
character_id.dart
import 'dart:convert';
import 'package:ceal_chronicler_f/utils/readable_uuid.dart';
import '../utils/json_serializable.dart';
class CharacterId extends JsonSerializable {
static const String _idFieldName = "id";
var id = ReadableUuid();
CharacterId();
CharacterId.fromJson(Map<String, dynamic> json)
: id = ReadableUuid.fromJson(json[_idFieldName]);
CharacterId.fromJsonString(String jsonString)
: this.fromJson(jsonDecode(jsonString));
#override
Map<String, dynamic> toJson() => {
_idFieldName: id,
};
[...]
}
This is already not bad, but there's still some duplication here. Both my concrete classes still need to have a fromJsonString constructor that is effectively identical, and they both still need to import dart:convert because of that. Also, I can't enforce that they have a fromJson constructor like that.
Now, what I would like to have is something like this:
json_serializable.dart
import 'dart:convert';
abstract class JsonSerializable {
abstract JsonSerializable.fromJson(Map<String, dynamic> json);
JsonSerializable.fromJsonString(String jsonString)
: this.fromJson(jsonDecode(jsonString));
Map<String, dynamic> toJson();
String toJsonString() => jsonEncode(toJson());
}
readable_uuid.dart
import 'package:uuid/uuid.dart';
import 'json_serializable.dart';
class ReadableUuid extends JsonSerializable {
static const Uuid uuidGenerator = Uuid();
String uuid = uuidGenerator.v4();
static const String _uuidFieldName = "uuid";
ReadableUuid();
#override
ReadableUuid.fromJson(Map<String, dynamic> json)
: uuid = json[_uuidFieldName];
#override
Map<String, dynamic> toJson() => {
_uuidFieldName: uuid,
};
[...]
}
character_id.dart
import 'package:ceal_chronicler_f/utils/readable_uuid.dart';
import '../utils/json_serializable.dart';
class CharacterId extends JsonSerializable {
static const String _idFieldName = "id";
var id = ReadableUuid();
CharacterId();
#override
CharacterId.fromJson(Map<String, dynamic> json)
: id = ReadableUuid.fromJson(json[_idFieldName]);
#override
Map<String, dynamic> toJson() => {
_idFieldName: id,
};
[...]
}
Now, I know this does not work because constructors are not part of the interface in Dart (and it's made more complicated by Flutter disallowing Runtime Reflection in favor of Tree Shaking). My question is, can anyone think of a workaround here that achieves as many of the following goals as possible:
No duplicate code in concrete classes
All dependencies on dart:convert are concentrated inside JsonSerializable
The fromJson constructor and toJson method are enforced to exist in concrete classes
I have now come up with a "solution" that addresses about 1.75 of the above requirements, but has some drawbacks.
First, here's the "solution":
json_serializable.dart
import 'dart:convert';
abstract class JsonSerializable {
JsonSerializable();
JsonSerializable.fromJsonString(String jsonString)
: this.fromJson(jsonDecode(jsonString));
JsonSerializable.fromJson(Map<String, dynamic> jsonMap){
decodeJson(jsonMap);
}
decodeJson(Map<String, dynamic> jsonMap);
Map<String, dynamic> toJson();
String toJsonString() => jsonEncode(toJson());
}
readable_uuid.dart
import 'package:uuid/uuid.dart';
import 'json_serializable.dart';
class ReadableUuid extends JsonSerializable {
static const Uuid uuidGenerator = Uuid();
String uuid = uuidGenerator.v4();
static const String _uuidFieldName = "uuid";
ReadableUuid();
ReadableUuid.fromJsonString(String jsonString)
: super.fromJsonString(jsonString);
ReadableUuid.fromJson(Map<String, dynamic> jsonMap) : super.fromJson(jsonMap);
#override
decodeJson(Map<String, dynamic> jsonMap) {
uuid = jsonMap[_uuidFieldName];
}
#override
Map<String, dynamic> toJson() => {
_uuidFieldName: uuid,
};
[...]
}
character_id.dart
import 'package:ceal_chronicler_f/utils/readable_uuid.dart';
import '../utils/json_serializable.dart';
class CharacterId extends JsonSerializable {
static const String _idFieldName = "id";
var id = ReadableUuid();
CharacterId();
CharacterId.fromJsonString(String jsonString)
: super.fromJsonString(jsonString);
CharacterId.fromJson(Map<String, dynamic> jsonMap) : super.fromJson(jsonMap);
#override
decodeJson(Map<String, dynamic> jsonMap) {
id = ReadableUuid.fromJson(jsonMap[_idFieldName]);
}
#override
Map<String, dynamic> toJson() => {
_idFieldName: id,
};
[...]
}
Pros
All dependencies on dart:convert are concentrated inside JsonSerializable
JsonSerializable enforces the creation of toJson amd decodeJson methods
Little boilerplate code
Cons
Only works if all fields of concrete classes are either initialized with default values, late or nullable
The conveyor-belt constructors need to be implemented every time even though they always look the same, which feels like a bit of duplication
That would be the best solution that I can come up at the moment. If anyone has a better solution, please feel free to share it here.
Related
I am using json annotation package to generate the json serialization boilerplate code. I have Base class
#JsonSerializable()
class UIWidget {
double? size = 50;
List<UIWidget> children = List.empty(growable: true);
factory UIWidget.fromJson(Map<String, dynamic> json) => _$UIWidgetFromJson(json);
Map<String, dynamic> toJson() => _$UIWidgetToJson(this);
}
I have several subclasses and one such is given below.
#JsonSerializable(explicitToJson: true)
class UIGridView extends UIWidget {
int scrollDirection = Axis.vertical.index;
String _showAxis = "Vertical";
int crossAxisCount = 3;
double mainAxisSpacing = 0.0;
double crossAxisSpacing = 0.0;
double childAspectRatio = 1.0;
factory UIGridView.fromJson(Map<String, dynamic> json) => _$UIGridViewFromJson(json);
Map<String, dynamic> toJson() => _$UIGridViewToJson(this);
}
If you notice, the UIWidget class has children property, which can contain any of the sub classes. The problem arise when I tried to generate the code. The fromJson method is generated as follows
..children = (json['children'] as List<dynamic>)
.map((e) => UIWidget.fromJson(e as Map<String, dynamic>))
.toList();
However, I needed to call the subclass fromjson and create instance of subclass. Is there a way to do this?
Where's your super() constructor? If you're extending UIWidget, you need to pass your params from the extending class to the parent class, and codegen will work as expected.
// Base class
#JsonSerializable()
class ParentClass {
final String? someString;
const ParentClass({
this.someString,
});
factory ParentClass.fromJson(Map<String, dynamic> json) =>
_$ParentClassFromJson(json);
Map<String, String> toJson() => Map.from(_$ParentClassToJson(this));
}
// Child class extending base
#JsonSerializable()
class ChildClass extends ParentClass {
final List<int> exampleList;
const ChildClass({
required this.exampleList,
String? someString,
}) : super(someString: someString);
factory ChildClass.fromJson(Map<String, dynamic> json) =>
_$ChildClassFromJson(json);
Map<String, String> toJson() =>
Map.from(_$ChildClassToJson(this));
}
Im sure you know the problem above. Im wondering how I can solve it. I understand that my data is in form of a list but inside the data class I used map. I don't really understand how I should change it to work, basically I just followed the flutter.dev documentation
So if you are wondering what I did
I basically parsed my data with json_serializable. In testing with test data all worked fine.
My data:
My model contains a title, image & a nested class called modelData.
`import 'package:json_annotation/json_annotation.dart';
import 'modelData.dart';
part 'Modell.g.dart';
#JsonSerializable(explicitToJson: true)
class Modell {
final String title;
final String image;
final ModelData modelData;
Modell(this.title, this.image, this.modelData);
factory Modell.fromJson(Map<String, dynamic> json)
=> _$ModellFromJson(json);
Map<String, dynamic> toJson() => _$ModellToJson(this);}
`
import 'package:json_annotation/json_annotation.dart';
part 'modelData.g.dart';
#JsonSerializable()
class ModelData {
final String title;
ModelData(this.title);
factory ModelData.fromJson(Map<String, dynamic> json)
=> _$ModelDataFromJson(json);
Map<String, dynamic> toJson() => _$ModelDataToJson(this);
}
Im consuming the data with this code:
var modelle = const[];
Future loadDataList() async {
String content = await rootBundle.loadString("assets/ddddddd.json");
List collection = json.decode(content);
List<Modell> _modelle = collection.map((json) => Modell.fromJson(json)).toList();
setState(() {
modelle = _modelle;
});
}
void initState() {
loadDataList();
super.initState();
& if needed here a part of my Data:
[
{
"title":" Alfa Romeo ",
"image":" AlfaRomeo.png ",
"modelData":[
{
"title":" 4C ",
"variantenData":[
]
},
I hope I wrote clear & detailed enough. If Im missing something, sry
-----Update-----
I tested a bit & found out that with simply adding List I no longer get the error, will test further to see whether the solution really works as intended
import 'package:json_annotation/json_annotation.dart';
import 'modelData.dart';
part 'Modell.g.dart';
#JsonSerializable(explicitToJson: true)
class Modell {
final String title;
final String image;
List <ModelData> modelData; //adding List
Modell(this.title, this.image, this.modelData);
factory Modell.fromJson(Map<String, dynamic> json)
=> _$ModellFromJson(json);
Map<String, dynamic> toJson() => _$ModellToJson(this);
}
You have parsed the data and all but the class named Modell should contain a list of ModelData and not a single ModelData. Please change the type to a list of ModelData and it should work hopefully.
class Modell {
final String title;
final String image;
final List<ModelData> modelDataList;
Modell(this.title, this.image, this.modelDataList);
factory Modell.fromJson(Map<String, dynamic> json)
=> _$ModellFromJson(json);
Map<String, dynamic> toJson() => _$ModellToJson(this);}```
In this part:
var modelle = const[];
Future loadDataList() async {
String content = await rootBundle.loadString("assets/ddddddd.json");
List collection = json.decode(content);
List<Modell> _modelle = collection.map((json) => Modell.fromJson(json)).toList();
setState(() {
modelle = _modelle;
});
}
void initState() {
loadDataList();
super.initState();
More specific here:
List collection = json.decode(content);
If you go to Flutter Example will see that collection will return a Map<String, dynamic>.
Try change to:
Map<String, dynamic> collection = json.decode(content);
I have a class A that contains a list of objects of another class B(Composition). Now, I want to store the object of class A to sqlite database. I learnt how to encode basic strings or integers to json but still could not find a way to encode a list of objects.
Need to save an object of concrete 'Layout'.
class MallardDuck extends Duck {
var image = "https://image.shutterstock.com/image-vector/cartoon-duck-swimming-600w-366901346.jpg";
Widget d_widget;
List<Widget> previous_states = [];
List<Widget> redo_states = [];
}
abstract class Layout extends Duck{
List<Duck> listOfDucks = [];
bool default_layout;
}
This might help you.
It is not the answer to your specific case but hopefully this code will help you see how I did it in a different class.
class BlogData {
final String title;
final String associatedBusinessId;
final String category;
final List<BlogParagraph> blogParagraphs;
BlogData(
{this.title,
this.associatedBusinessId,
this.category,
this.blogParagraphs});
Map<String, dynamic> toJson() {
return {
'title': this.title,
'associatedBusinessId': this.associatedBusinessId,
'category': this.category,
'blogParagraphs':
blogParagraphs.map((paragraph) => paragraph.toJson()).toList()
};
}
}
Then, in the blog paragraphs class:
class BlogParagraph {
final int paragraphId;
final String content;
final Set<int> imageRefs;
BlogParagraph({this.paragraphId, this.content, this.imageRefs});
Map<String, dynamic> toJson() {
return {
'id': this.paragraphId,
'content': this.content,
'imageRefs': imageRefs.isEmpty ? [] : imageRefs.toList(),
};
}
}
I'm using automatic serialization/deserialization in dart like mentioned here
import 'package:json_annotation/json_annotation.dart';
part 'billing.g.dart';
#JsonSerializable()
class Billing {
Billing(){}
String _id;
String name;
String status;
double value;
String expiration;
factory Billing.fromJson(Map<String, dynamic> json) => _$BillingFromJson(json);
Map<String, dynamic> toJson() => _$BillingToJson(this);
}
But in order for the serialization/deserialization to work, the fields must be public. However, in Dart, a field with _ at the beggining is private. So I can't use _id from mongodb to serialize/deserialize things.
How can I overcome this?
You can use #JsonKey annotation. Refer https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/name.html
import 'package:json_annotation/json_annotation.dart';
part 'billing.g.dart';
#JsonSerializable()
class Billing {
Billing(){}
// Tell json_serializable that "_id" should be
// mapped to this property.
#JsonKey(name: '_id')
String id;
String name;
String status;
double value;
String expiration;
factory Billing.fromJson(Map<String, dynamic> json) => _$BillingFromJson(json);
Map<String, dynamic> toJson() => _$BillingToJson(this);
}
I have a class:
import 'package:google_maps_flutter/google_maps_flutter.dart';
class Place {
Place({
this.address,
this.coordinates,
});
final String address;
final LatLng coordinates;
}
LatLng is a class of google_maps_flutter. How can I make my Place class serializable using json_annotation and json_serializable?
Thank you very much!
You can explicitly specify which methods of LatLng should be used for serialization using JsonKey annotation:
#JsonSerializable()
class Place {
Place({
this.address,
this.coordinates,
});
final String address;
#JsonKey(fromJson: LatLng.fromJson, toJson: jsonEncode)
final LatLng coordinates;
}
Put this code in your model
to get the information from JSON response simply do that after your request
final place = placeFromJson(response.body);
to get address => = place.address
to get coordinates => place.coordinates.lng , place.coordinates.lat
=============================================
import 'dart:convert';
Place placeFromJson(String str) => Place.fromJson(json.decode(str));
String placeToJson(Place data) => json.encode(data.toJson());
class Place {
String address;
Coordinates coordinates;
Place({
this.address,
this.coordinates,
});
factory Place.fromJson(Map<String, dynamic> json) => Place(
address: json["address"],
coordinates: Coordinates.fromJson(json["coordinates"]),
);
Map<String, dynamic> toJson() => {
"address": address,
"coordinates": coordinates.toJson(),
};
}
class Coordinates {
String lat;
String lng;
Coordinates({
this.lat,
this.lng,
});
factory Coordinates.fromJson(Map<String, dynamic> json) => Coordinates(
lat: json["lat"],
lng: json["lng"],
);
Map<String, dynamic> toJson() => {
"lat": lat,
"lng": lng,
};
}
I didn't find a super easy solution for this. I ended up, writing a custom toJson/fromJson for the LatLng property. I've put it statically onto my model, but you could also create a global function, if you need to reuse it.
Take care, that in my example LatLng is nullable.
import 'dart:collection';
import 'dart:convert';
import 'package:latlong2/latlong.dart';
part 'challenge_model.g.dart';
#JsonSerializable()
class ChallengeModel with _$ChallengeModel {
ChallengeModel({
required this.startPosition,
});
#override
#JsonKey(fromJson: latLngFromJson, toJson: latLngToJson)
final LatLng? startPosition;
/// Create [ChallengeModel] from a json representation
static fromJson(Map<String, dynamic> json) => _$ChallengeModelFromJson(json);
/// Json representation
#override
Map<String, dynamic> toJson() => _$ChallengeModelToJson(this);
static String latLngToJson(LatLng? latLng) =>
jsonEncode(latLng != null ? {'latitude': latLng.latitude, 'longitude': latLng.longitude} : null);
static LatLng? latLngFromJson(String jsonString) {
final LinkedHashMap<String, dynamic>? jsonMap = jsonDecode(jsonString);
return jsonMap != null ? LatLng(jsonMap['latitude'], jsonMap['longitude']) : null;
}
}