Freezed - creating subclasses - flutter

I have a JSON data structure from an API, which has some fields in it.
I want to create a Freezed model that has a factory to create the correct subclass, depending on one of the fields' type.
Here is my code:
abstract class ItineraryData<T> implements _$ItineraryData<T> {
const ItineraryData._();
const factory ItineraryData({
List<dynamic> summary,
double totalPrice,
double totalSeparatePay,
double totalTax,
List<String> covidAlerts,
}) = _ItineraryData;
const factory ItineraryData.flight({
List<FlightSummary> summary,
double totalPrice,
double totalSeparatePay,
double totalTax,
List<String> covidAlerts,
}) = _FlightItineraryData;
const factory ItineraryData.hotel({
List<HotelSummary> summary,
double totalPrice,
double totalSeparatePay,
double totalTax,
}) = _HotelItineraryData;
const factory ItineraryData.car({
List<CarSummary> summary,
double totalPrice,
double totalSeparatePay,
double totalTax,
List<String> covidAlerts,
}) = _CarItineraryData;
factory ItineraryData.fromJson(Map json) => _$ItineraryDataFromJson(json);
}
When it's this way, accessing a reference of ItineraryData<A> would never have a summary list field (which should always be there, but with different type).
Then I tried to change the unnamed constructor to use <T> in the List type:
const factory ItineraryData({
List<T> summary,
double totalPrice,
double totalSeparatePay,
double totalTax,
List<String> covidAlerts,
}) = _ItineraryData;
But then I get the following error:
Could not generate `fromJson` code for `summary` because of type `T` (type parameter).
None of the provided `TypeHelper` instances support the defined type.
To support type paramaters (generic types) you can:
1) Use `JsonConverter`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
2) Use `JsonKey` fields `fromJson` and `toJson`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
3) Set `JsonSerializable.genericArgumentFactories` to `true`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonSerializable/genericArgumentFactories.html
package:<???>/models/itinerary_data.freezed.dart:253:17
╷
253 │ final List<T> summary;
│ ^^^^^^^
╵
[INFO] Running build completed, took 221ms
I did try adding a converter, like so:
class ItineraryDataConverter
implements JsonConverter<ItineraryData, Map<String, dynamic>> {
const ItineraryDataConverter();
#override
ItineraryData fromJson(Map<String, dynamic> json) {
if (json == null || !json.containsKey('data')) {
return null;
}
json = json['data'];
// type data was already set (e.g. because we serialized it ourselves)
if (!json.keys
.any((k) => ['cabin_class', 'hotel', 'car_type'].contains(k))) {
return ItineraryData.fromJson(json);
}
// you need to find some condition to know which type it is. e.g. check the presence of some field in the json
if (json.containsKey('cabin_class')) {
return _FlightItineraryData.fromJson(json);
} else if (json.containsKey('hotel')) {
return _HotelItineraryData.fromJson(json);
} else if (json.containsKey('car_type')) {
return _CarItineraryData.fromJson(json);
} else {
throw Exception(
'Could not determine the constructor for mapping from JSON');
}
}
#override
Map<String, dynamic> toJson(ItineraryData data) => data.toJson();
}
But I still get the same error.
How can I make sure summary is visible + strongly typed from subclasses?
Or what would a better approach to this be?

Have you try adding custom converter for type T?
const factory ItineraryData({
#CustomConverter() List<T> summary,
...
...
class CustomConverter<T> implements JsonConverter<T, Object> {
const CustomConverter();
#override
T fromJson(Object json) {
// TODO: implement fromJson
throw UnimplementedError();
}
#override
Object toJson(T object) {
// TODO: implement toJson
throw UnimplementedError();
}
}

Related

Why I am getting Instance members can't be accessed from a factory constructor?

I am getting following errors:
Instance members can't be accessed from a factory constructor. (Documentation) Try removing the reference to the instance member.
The argument type 'List<Map<String, dynamic>>?' can't be assigned to the parameter type 'List<Vaccination>'. (Documentation)
at line _convertVaccinations(json['vaccinations'] as List<dynamic>));
Code:
final String name;
final String? notes;
final String type;
final List<Vaccination> vaccination;
final String? referenceId;
Pet(this.name, {this.notes, required this.type, required this.vaccination, this.referenceId});
factory Pet.fromJson(Map<String, dynamic> json) =>
Pet(
json['name'] as String,
notes: json['notes'] as String,
type: json['types'] as String,
referenceId: json['referenceId'] as String,
vaccination:
_convertVaccinations(json['vaccinations'] as List<dynamic>));
List<Map<String, dynamic>>? _convertVaccinations(List<dynamic>? vaccinations) {
if (vaccinations == null) {
return null;
} else {
final vaccinationMap = <Map<String, dynamic>>[];
for (var element in vaccinations) {
vaccinationMap.add(element.toJson);
}
return vaccinationMap;
}
}
Factory and instance member error:
Well it is because factory Pet.fromJson(...) is a factory constructor, and the class method _convertVaccinations(...) is an instance member.
If you make _convertVaccinations(...) static, it will accept the use of it in the factory constructor.
Argument error:
vaccination is of type List<Vaccination> and the method _convertVaccination(...) returns either null or List<Map<String, dynamic>>
In other words, you cannot assign null to List<T> unless it says List<T>? and the class Vaccination is not a Map<String, dynamic>
Maybe you want to do something like final List<Vaccination>? vaccinations; OR return <Vaccination>[] instead of null if vaccinations == null.
So you'd probably want to do write _convertVaccinations(...) as:
static List<Vaccination>? _convertVaccination(List<dynamic>? vaccinations) {
return vaccinations?.map((e) => Vaccination.fromJson(e as Map<String,dynamic>).toList();
}
or:
static List<Vaccination> _convertVaccination(List<dynamic>? vaccinations) {
return vaccinations?.map((e) => Vaccination.fromJson(e as Map<String,dynamic>).toList() ?? <Vaccination>[];
}
Side note: Maybe you have more methods that you haven't presented here. Because it looks a bit wonky when your Pet.fromJson(...) use a element.toJson down the call stack.

Fauna DB and flutter, unable to get a default value on Select using the faunadb_http package

I am using the faunadb_http package and I want the value to be returned null from Fauna DB if the field does not exist in the collection. I am just not able to figure out what should I put in the default parameter of this package so that I get that back as the default value.
I tried the following two variations of default parameter and I get "Value not found at path" error for first and just an empty Object {} for second.
'itemPrice': Select(["data", "itemPrice"], Var("postDoc"), default_: null),
'itemLocation': Select(["data", "itemLocation"], Var("postDoc"), default_: Obj({})),
Can somebody help me understand what should I be passing to default_ so that I get a String or Int as a response back.
This is the code for the Select class from the package
#JsonSerializable()
class Select extends Expr {
#JsonKey(name: 'select')
final Object path;
final Expr from;
#JsonKey(name: 'default', disallowNullValue: true, includeIfNull: false)
final Expr? default_;
Select(this.path, this.from, {this.default_});
factory Select.fromJson(Map<String, dynamic> json) => _$SelectFromJson(json);
#override
Map<String, dynamic> toJson() => _$SelectToJson(this);
}
And this is for the Expr class
class Expr {
static Object? wrap_value(dynamic value) {
if (value is List) {
return wrap_values(value);
} else if (value is Map<String, dynamic>) {
return Obj(value);
} else if (value is DateTime) {
return Time(value.toUtc().toIso8601String());
} else {
return value;
}
}
static Object? wrap_values(Object? data) {
if (data == null) return null;
if (data is List) {
return List.generate(
data.length,
(e) => wrap_value(data[e]),
growable: false,
);
} else if (data is Map<String, dynamic>) {
return data.map(
(key, value) => MapEntry(key, wrap_value(value)),
);
}
return data;
}
Expr();
factory Expr.fromJson(Map<String, dynamic> json) => _$ExprFromJson(json);
Map<String, dynamic> toJson() => _$ExprToJson(this);
#override
String toString() {
return json.encode(this).toString();
}
}
I'm going to set aside the language-specific aspects, as I'm not familiar with Dart.
That said, as I read through your post it seems like Select() is working as defined. The third argument is what is returned if your data is not found, e.g., null.
In the first case, you are returning null explicitly, and Fauna removes keys with null values, so that value would indeed not be found.
In the second case, you are returning an empty Object, and you receive an empty Object, so that seems to be working as defined as well.
Can somebody help me understand what should I be passing to default_ so that I get a String or Int as a response back.
In this case you need to explicitly set an Expr that will evaluate to a string or Int. If the empty string "" and zero 0 are reasonable defaults, then you would want:
'itemPrice': Select(["data", "itemPrice"], Var("postDoc"), default_: 0),
and
'itemLocation': Select(["data", "itemLocation"], Var("postDoc"), default_: ""),
I got in touch with the author of the package and they were kind enough to fix the issue within a day of reporting it. Now it works as expected.

freezed how to assign my own JsonConverter on top level model?

I have freezed model (simplified):
part 'initial_data_model.freezed.dart';
part 'initial_data_model.g.dart';
#freezed
class InitialDataModel with _$InitialDataModel {
const factory InitialDataModel() = Data;
const factory InitialDataModel.loading() = Loading;
const factory InitialDataModel.error([String? message]) = Error;
factory InitialDataModel.fromJson(Map<String, dynamic> json) => _$InitialDataModelFromJson(json);
}
documentation says how to assign custom converters on fields but not on model itself
I got json from backend and somewhere in api_provider I do
return InitialDataModel.fromJson(json);
I have no control on json structure, there aren't "runtimeType" and other stupid redundant things
when I want to create a model from json I call fromJson I'm having this
flutter: CheckedFromJsonException
Could not create `InitialDataModel`.
There is a problem with "runtimeType".
Invalid union type "null"!
ok, again
I have api_provider
final apiProvider = Provider<_ApiProvider>((ref) => _ApiProvider(ref.read));
class _ApiProvider {
final Reader read;
_ApiProvider(this.read);
Future<InitialDataModel> fetchInitialData() async {
final result = await read(repositoryProvider).send('/initial_data');
return result.when(
(json) => InitialDataModel.fromJson(json),
error: (e) => InitialDataModel.error(e),
);
}
}
you may see I'm trying to create InitialDataModel from json
this line throws an error I mentioned above
I don't understand how to create InitialDataModel from json, now in my example it's just empty model, there are no fields
(json) => InitialDataModel.fromJson(json),
json here is Map, it shows an error even if I pass simple empty map {} instead of real json object
The easiest solution is to use the correct constructor instead of _$InitialDataModelFromJson. Example:
#freezed
class InitialDataModel with _$InitialDataModel {
const factory InitialDataModel() = Data;
...
factory InitialDataModel.fromJson(Map<String, dynamic> json) => Data.fromJson(json);
}
The drawback of course is that you can only use the fromJson when you're sure you have the correct json, which isn't great. I actually wouldn't recommend this way because it leaves to the caller the burden of checking the validity and calling the correct constructor.
Another solution, maybe the best, is to follow the documentation and create a custom converter, even though this would require you to have two separated classes.
Otherwise you could chose a different approach and separate the data class from the union, so you'll have a union used just for the state of the request and a data class for the success response:
#freezed
class InitialDataModel with _$InitialDataModel {
factory InitialDataModel(/* here go your attributes */) = Data;
factory InitialDataModel.fromJson(Map<String, dynamic> json) => _$InitialDataModelFromJson(json);
}
#freezed
class Status with _$Status {
const factory Status.success(InitialDataModel model) = Data;
const factory Status.loading() = Loading;
const factory Status.error([String? message]) = Error;
}
and then
[...]
return result.when(
(json) => Status.success(InitialDataModel.fromJson(json)),
error: (e) => Status.error(e),
);
[...]

json = null in fromJSON method in custom JsonConverter " freezed class with multiple constructors "

I have this class
#freezed
abstract class CartEntity with _$CartEntity {
const factory CartEntity.empty(String status, String message) = _Empty;
const factory CartEntity.notEmpty(int x) = _NotEmpty;
factory CartEntity.fromJson(Map<String, dynamic> json) =>
_$CartEntityFromJson(json);
}
And this converter
class CartEntityConverter
implements JsonConverter<CartEntity, Map<String, dynamic>> {
const CartEntityConverter();
#override
CartEntity fromJson(Map<String, dynamic> json) {
//the problem here
print(json);// null
return _Empty.fromJson(json);
}
#override
Map<String, dynamic> toJson(CartEntity object) {
return object.toJson();
}
}
And this wrapper class
#freezed
abstract class CartEntityWrapper with _$CartEntityWrapper {
const factory CartEntityWrapper(#CartEntityConverter() CartEntity cartEntity) =
CartEntityWrapperData;
factory CartEntityWrapper.fromJson(Map<String, dynamic> json) =>
_$CartEntityWrapperFromJson(json);
}
And iam called
final cartEntity = CartEntityWrapperData.fromJson({'x':'y'});
print(cartEntity);
fromJson method which in CartEntityConverter is always receive null json so what's i made wrong ?
Instead of making yet another converter class that you use directly, you could just add .fromJsonA method in the main class.
It will looks like this one:
#freezed
abstract class CartEntity with _$CartEntity {
const factory CartEntity.empty(String status, String message) = _Empty;
const factory CartEntity.notEmpty(int x) = _NotEmpty;
factory CartEntity.fromJson(Map<String, dynamic> json) =>
_$CartEntityFromJson(json);
factory CartEntity.fromJsonA(Map<String, dynamic> json) {
if (/*condition for .empty constructor*/) {
return _Empty.fromJson(json);
} else if (/*condition for .notEmpty constructor*/) {
return _NotEmpty.fromJson(json);
} else {
throw Exception('Could not determine the constructor for mapping from JSON');
}
}
}
solved by using
final cartEntity = CartEntityConverter().fromJson({'x':'y'});
print(cartEntity);
instead of
final cartEntity = CartEntityWrapperData.fromJson({'x':'y'});
print(cartEntity);
documentation have a lack at this point i tried random stuffs to make it work

Add comma separated value to class list

I need to add the value in the list which is comma separated.
Sample data:
English,Hindi,French
Below is the class of List:
class LanguageService {
}
class Language extends Taggable {
final String name;
/// Creates Language
Language({
this.name,
// this.position,
});
#override
List<Object> get props => [name];
/// Converts the class to json string.
String toJson() => ''' {
"name": $name,\n
}''';
//}
String thuJson() => ''' {
"name": $name,
}''';
}
GetTags getTagsFromJson(String str) => GetTags.fromJson(json.decode(str));
class GetTags {
List<Content> content;
bool success;
//String error;
GetTags({
this.content,
this.success,
});
factory GetTags.fromJson(Map<String, dynamic> json) => GetTags(
content: (json["content"] as List).map((x) => Content.fromJson(x)).toList(),
success: json["success"],
);
}
class Content {
String tagname;
Content({
this.tagname,
});
factory Content.fromJson(Map<String, dynamic> json) => Content(
tagname: json == null ? 'Empty' : json["tagname"]
);
}
I tried split but it is giving me error.
List<Language> _selectedLanguages;
_selectedLanguages = [];
//responseBody['user_lang'] = 'English,Hindi,French' Data looks like this
_selectedLanguages = responseBody['user_lang'].split(', ');
Exception Caught: type 'List<String>' is not a subtype of type 'List<Language>'
Also tried.
_selectedLanguages.add(responseBody['user_lang']);
Exception Caught: type 'String' is not a subtype of type 'Language'
Update
I tried this too but getting error.
List _dbLanguages = responseBody['user_lang'].split(', ');
selectedLanguages = _dbLanguages.map<List<Language>>((item) => Language(item))
A value of type 'Iterable<List<Language>>' can't be assigned to a variable of type 'List<Language>'.
Try changing the type of the variable, or casting the right-hand type to 'List<Language>'.
One way you can do this is like this.
List<Language> _selectedLanguages;
_selectedLanguages = (responseBody['user_lang'].split(',') as List<String>).map((text) => Language(name: text)).toList();
Dart has a very good type checking system and I think your problem is an obvious one based on the error message. You must convert your list of String into a list of Language. I don't recall the syntax from the top of my head but I think you should be able to convert your list of String with .map<List<Language>>((item) => Language(item))