This question already has an answer here:
Complex models with json_serializable - List<objects> not converting to map
(1 answer)
Closed 5 months ago.
Background
I'm trying to use the dart json_serializable package to encode/decode a custom type I'm writing/reading with Google Cloud Firestore, and I'm having an issue with the custom type having properties that are also custom types.
I'm storing a Habit which has two custom properties:
HabitFrequency is a combination of an integer, and an enum frequencyType that I want to store together as a map in Firestore.
HabitCompletion is a list of the type HabitCompletion, which contains a date, completion status, and an optional string value/numeric value. I want this to be stored as an array of maps.
Issue
Whenever I try to save, I get this error:
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument: Instance of 'HabitFrequency'
I have the top-level class and both custom classes used as properties (HabitFrequency and HabitCompletion) listed as JsonSerializable, and they have the appropriate fromJson/toJson methods specified.
Question
Is there a different way I need to set these up before I run build_runner build to generate the right code to serialize these properly?
Code Examples
Here is my top-level class:
#JsonSerializable()
class Habit {
String? id;
String title;
#JsonKey(name: 'frequency')
HabitFrequency frequency;
List<HabitCompletion>? completions;
Habit({
this.id,
required this.title,
required this.frequency,
this.completions,
});
/// Connect the generated [_$PersonFromJson] function to the `fromJson`
/// factory.
factory Habit.fromJson(Map<String, dynamic> json) => _$HabitFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$HabitToJson(this);
}
}
The Habit Frequency:
#JsonSerializable()
class HabitFrequency {
int amount;
HabitFrequencyType frequencyType;
HabitFrequency(this.amount, this.frequencyType);
/// Connect the generated [_$HabitFrequencyFromJson] function to the `fromJson`
/// factory.
factory HabitFrequency.fromJson(Map<String, dynamic> json) =>
_$HabitFrequencyFromJson(json);
/// Connect the generated [_$HabitFrequencyToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$HabitFrequencyToJson(this);
}
And the Habit completion:
#JsonSerializable()
class HabitCompletion {
DateTime timestamp;
bool completion;
String? stringValue;
double? numericValue;
HabitCompletion(
{required this.timestamp,
required this.completion,
this.stringValue,
this.numericValue});
/// Connect the generated [_$PersonFromJson] function to the `fromJson`
/// factory.
factory HabitCompletion.fromJson(Map<String, dynamic> json) =>
_$HabitCompletionFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$HabitCompletionToJson(this);
}
They each have one property that is just a simple enumeration.
If you want to see more of the code, here's a gist containing the file for this type (and the custom types used for the properties in questions), as well as the auto-generated code coming from json_serializable.
this can be resolved by setting explicitToJson on the JsonSerializable annotation for the Habit class as mentioned in your gist
#JsonSerializable(explicitToJson: true)
class Habit{
....
}
u should be getting syntax as some properties are not defined but u are using them in the constructor?
#JsonSerializable()
class Habit {
String? id;
String title;
#JsonKey(name: 'frequency')
HabitFrequency frequency;
List<HabitCompletion>? completions;
Habit({
this.id,
required this.title,
// not defined??????
this.description,
// not defined?????? //etc...
required this.userId,
this.attachedDashboardId,
this.attachedGoalId,
required this.startDate,
this.endDate,
this.isActive = true,
this.positivity = HabitPositivity.positive,
this.frequency,
this.completions,
});
/// Connect the generated [_$PersonFromJson] function to the `fromJson`
/// factory.
factory Habit.fromJson(Map<String, dynamic> json) => _$HabitFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$HabitToJson(this);
}
}
Related
I make an API request and get data. I have created models in which the received values are stored. I'm using the gallery variable to store a list of photo links. Tell me, how can I access an element from this list of the gallery variable in order to display a photo by link?
model
class User {
final int id;
final List<PhotoModel>? gallery;
User({
required this.id,
this.gallery,
});
factory User.fromJson(Map<String, dynamic> json) {
List<PhotoModel> ph = [];
json['gallery'].forEach((v) {
ph.add(PhotoModel.fromJson(v));
});
return User(
id: json['id'] as int,
gallery: ph,
);
}
}
model2
class PhotoModel {
final String url;
PhotoModel({required this.url});
factory PhotoModel.fromJson(Map<String, dynamic> json) {
return PhotoModel(
url: json['url'],
);
}
}
Here is what I get
You print out user.gallary which is a list of PhotoModel instances. So your output makes sense it seems so that your gallery contains one PhotoModel.
If you want a specific output for each PhotoModel you have several options:
Override the toString() in the PhotoModel class and return the url
Change your print statement so it directly prints the url instead of the object itself. print(widget.state.user!.gallery.map((p) => p.url));
Another alternative is to add an toJson() method to your class which works the other way around like fromJson() and returns a map. If you call this method and print the map every attribute of the model will be visible.
I have a class which I am trying to use with Freezed, Json Serializable, and Hive. After running dart run build_runner build and generating the necessary classes, my compiler gives me the following error:
: Error: Can't use '_$FooBarFromJson' because it is declared more than once.
and
: Error: '_$FooBarFromJson' is already declared in this scope.
part 'foobar.freezed.dart';
part 'foobar.g.dart';
#freezed
#JsonSerializable(explicitToJson: true)
#HiveType(typeId: 0)
class FooBar extends HiveObject with _$FooBar {
factory FooBar({
#HiveField(0) required int baz
}) = _FooBar;
factory FooBar.fromJson(Map<String, dynamic> json) =>
_$FooBarFromJson(json);
}
}
After looking through the generated classes, my foobar.g.dart file contains the following methods:
FooBar _$FooBarFromJson(Map<String, dynamic> json) => FooBar(
baz: json['baz'] as int,
);
Map<String, dynamic> _$FooBarToJson(FooBar instance) =>
<String, dynamic>{
'baz': instance.baz,
};
_$_FooBar _$$_FooBarFromJson(Map<String, dynamic> json) =>
_$_FooBar(
baz: json['baz'] as int,
);
Map<String, dynamic> _$$_FooBarToJson(_$_FooBar instance) =>
<String, dynamic>{
'baz': instance.baz,
};
And my foobar.freezed.dart contains this method:
FooBar _$FooBarFromJson(Map<String, dynamic> json) {
return _FooBar.fromJson(json);
}
I've noticed that other files that get converted only have the methods with the _$$_ prefix in foobar.g.dart, whereas _$FooBarFromJson is being created in both foobar.freezed.dart and foobar.g.dart, which is the cause of the errors. What am I missing here?
According to the following this comment in a freezed issue and as shown in the package readme example, you need to place the #JsonSerializable(explicitToJson: true) inside the class in the following manner:
part 'foobar.freezed.dart';
part 'foobar.g.dart';
#freezed
#HiveType(typeId: 0)
class FooBar extends HiveObject with _$FooBar {
#JsonSerializable(explicitToJson: true) // <~~~ here
factory FooBar({
#HiveField(0) required int baz
}) = _FooBar;
factory FooBar.fromJson(Map<String, dynamic> json) =>
_$FooBarFromJson(json);
}
}
You will notice this will give you the following warning:
The annotation 'JsonSerializable' can only be used on classes
This is a known issue/limitation and the maintainer recommends disabling that specific warning as discussed here.
I believe an alternative approach is to create build.yaml and specify explicitToJson in there but I don't have much knowledge there.
I'm using the freezed package to work with immutable models and make use of the built-in feature for json serialization by the json_serializable package. I have a simple User class/model with different union types (UserLoggedIn, UserGeneral, UserError):
#freezed
class User with _$User {
const factory User(String id, String email, String displayName) =
UserLoggedIn;
const factory User.general(String email, String displayName) = UserGeneral;
const factory User.error(String message) = UserError;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Since I'm using multiple constructors and don't want my API to include the runtimeType key as suggested by the documentation, I can write a converter (scroll a bit more down, sentence starts with: If you don't control the JSON response, then you can implement a custom converter.).
So based on that I wrote the following converter class:
class UserConverter implements JsonConverter<User, Map<String, dynamic>> {
const UserConverter();
#override
User fromJson(Map<String, dynamic> json) {
if (json['id'] != null) {
return UserLoggedIn.fromJson(json);
} else if (json['error'] != null) {
return UserError.fromJson(json);
} else {
return UserGeneral.fromJson(json);
}
}
#override
Map<String, dynamic> toJson(User data) => data.toJson();
}
The documentation now references another class (a wrapper class) which would now use this converter via annotation, something like this:
#freezed
class UserModel with _$UserModel {
const factory UserModel(#UserConverter() User user) = UserModelData;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
Question: is it possible to make use of this converter without having to use a wrapper class (UserModel)?
Reasoning: this wrapper class is adding another layer of abstraction which is not needed (in my cases). Especially since the wrapper class does not have any other benefit / purpose and it feels like it should be possible to do that without using it.
I am new to Flutter.
I am creating a named constructor to work with flutter Models. But for some reason it is showing an error:
class ImageModel {
int id;
String url;
String title;
// constructor
ImageModel(this.id, this.url, this.title);
// named constructor
ImageModel.fromJSON(Map<String, dynamic> parsedJson) {
id = parsedJson['id'];
url = parsedJson['url'];
title = parsedJson['title'];
}
}
Error:
Non-nullable instance field 'url' must be initialized.
Try adding an initializer expression, or add a field initializer
in this constructor, or mark it 'late'.dartnot_initialized_non_nullable_instance_field
I read the documentation, and found this solution, not sure why this is required at this place. I know its use case, but should not this work without this ?
class ImageModel {
late int id; // refactor
late String url; // refactor
late String title; // refactor
.
.
.
You have used incorrect syntax for the named constructor.
Instead of
ImageModel.fromJSON(Map<String, dynamic> parsedJson) {
id = parsedJson['id'];
url = parsedJson['url'];
title = parsedJson['title'];
}
it must be
ImageModel.fromJSON(Map<String, dynamic> parsedJson) :
id = parsedJson['id'],
url = parsedJson['url'],
title = parsedJson['title'];
The object is initialized after colon(:) in named constructor and curly brackets({}) are then used if you want to perform some task after initialization of object. Since you directly used {} after named constructor, it created an empty object for you and hence all parameters were null which you were trying to initialize in the function body. That's why this issue was solved after using 'late' keyword.
do you like this way
factory ImageModel.fromJson(Map<String, dynamic> json) {
return ImageModel(
json["id"],
json["url"],
json["title"],
);
}
And i prefer
class ImageModel {
int id;
String url;
String title;
// constructor
ImageModel({
required this.id,
required this.url,
required this.title,
});
factory ImageModel.fromJson(Map<String, dynamic> json) {
return ImageModel(
id: json["id"],
url: json["url"],
title: json["title"],
);
}
}
The Dart compiler complains because of its "null safety" feature. This means, that variable types like int or String must be initialised. This is been checked for at compile time. You can either add required in the constructor
ImageModel({
required this.id,
required this.url,
required this.title,
});
so that you cannot call the constructor without initialising the fields or let the compiler know that you will take care of the initialisation later by adding the late keyword (as you did). Of course you can also initialise the variables with some default values, if there are such
int id = 0;
String url = "https://www.example.com/default.jpg";
String title = "Default Text";
but that seems to be more of a corner case.
I am setting up my model classes to confirm to the docs for sqflite which suggest including a named constructor to convert to/from Maps to better handling of data between the classes and the DB. Every example I can find is very simple, with class properties all being simple data types.
Using the constructor and method shown below, converting to/from Map is quite simple when dealing with a class such as this.
class Human{
final String name;
final String height;
Final String weight;
Human({this.name, this.height, this.weight});
}
However, when you have a class where one of the fields is a bit more complex, I do not understand how to structure things within the named constructor and xxx method to return the map of data that I 'believe' I should get.
class Human{
final String name;
final String height;
Final String weight;
List<Child> children = [];
Human({this.name, this.height, this.weight, this.children});
}
Human({this.name, this.height, this.weight, this.children});
Human.fromMap(Map<String, dynamic> map)
: name = map['name'],
height = map['height'],
weight = map['weight'],
children = map['children'];
Map<String, dynamic> toMap() {
return {
'name': name,
'height': height,
'weight': weight,
'children': children,
};
}
The List children is the part I am struggling with. I believe you have to get each Child object ALSO converted to a map within the parent map, but am losing the battle here.
Is my approach way off here? Is there some other method I should be using to accomplish this?
Any assistance would be much appreciated.
Here I am explaining the following
How to convert a model object into Map to use with sqlite
How to convert a Map object from sqlite into a model class.
How to parse JSON reponse properly in flutter
How to convert a model object into JSON
All of the above questions has same answer. Dart has great support for these operations. Here I am going to illustrate it with a detailed example.
class DoctorList{
final List<Doctor> doctorList;
DoctorList({this.doctorList});
factory DoctorList.fromMap(Map<String, dynamic> json) {
return DoctorList(
doctorList: json['doctorList'] != null
? (json['doctorList'] as List).map((i) => Doctor.fromJson(i)).toList()
: null,
);
}
Map<String, dynamic> toMap() {
final Map<String, dynamic> data = Map<String, dynamic>();
if (this.doctorList != null) {
data['doctorList'] = this.doctorList.map((v) => v.toMap()).toList();
}
return data;
}
}
The above DoctorList class has a member which holds a list of 'Doctor' objects..
And see how I parsed the doctorList.
doctorList: json['doctorList'] != null
? (json['doctorList'] as List).map((i) => Doctor.fromMap(i)).toList()
: null,
You may wonder, how the Doctor class may look like. Here you go
class Doctor {
final String doCode;
final String doctorName;
Doctor({this.doCode, this.doctorName});
factory Doctor.fromMap(Map<String, dynamic> json) {
return Doctor(
doCode: json['doCode'],
doctorName: json['doctorName'],
);
}
Map<String, dynamic> toMap() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['doCode'] = this.doCode;
data['doctorName'] = this.doctorName;
return data;
}
}
That's all. Hope you got the idea. Cheers!