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

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),
);
[...]

Related

Generated class from freezed creates duplicate FromJson method

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.

Custom json converter with freezed without a wrapper class

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.

Is there any way to dynamically call a static method [duplicate]

This question already has an answer here:
Calling method on generic type Dart
(1 answer)
Closed 1 year ago.
I want to make APIHelper class.
This class will have method like get, post, put and delete.
And in these method all logic about getting data, decoding, encoding, mapping will be done.
I have base model class like this:
class Model{
Model();
Model.fromJson(Map<String, dynamic> data);
Map<String, dynamic> toJson(){
return {};
}
}
And in API model Event I inherited a class Model:
class EventModel extends Model{
final int desavanjeId;
final String desavanjeName;
EventModel({required this.desavanjeId, required this.desavanjeName});
#override
factory EventModel.fromJson(Map<String, dynamic> data) => EventModel(
desavanjeId: data['desavanjeId'],
desavanjeName: data['desavanjeName'],
);
#override
Map<String, Object> toJson() => {
'desavanjeId': this.desavanjeId,
'desavanjeName': this.desavanjeName,
};
}
And in service I have something like this:
Future<APIResponseModel> get<T>(Uri uri) async{
APIResponseModel apiRespone = APIResponseModel();
try {
Response response = await _client.get(uri);
Map<String, dynamic> data = jsonDecode(response.body);
apiRespone.addData(T.fromJson(data));
} catch (e) {
print(e);
}
return apiRespone;
}
And I am willing to use method get in this way:
get<EventModel>(Uri('...'));
But the problem is that IDE doesn't allow me to use static method fromJson in this way I need.
And I don't want to solve this problem in this way:
switch(Model){
case EventModel:
EventModel.fromJson(data)
}
Is there any other solution for this, but to keep a syntax in this way?
You cannot call a factory constructor or a static method from a generic type in Dart. Your only solution to obtain a similar result would be to use a callback method which will create your object. Here is a possible implementation you could use:
Code Sample
/// By looking at your implementation the Model class should be
/// abstract as it is your base model and should not be able to
/// be instantiated.
abstract class Model {
// fromJson is removed as it will be a static method
Map<String, dynamic> toJson();
}
class EventModel extends Model {
final int desavanjeId;
final String desavanjeName;
EventModel({required this.desavanjeId, required this.desavanjeName});
/// fromJson is now a static method which will return an instance of
/// your constructor so you can still call it like
/// this: EventModel.fromJson()
static EventModel fromJson(Map<String, dynamic> data) => EventModel(
desavanjeId: data['desavanjeId'],
desavanjeName: data['desavanjeName'],
);
#override
Map<String, Object> toJson() => {
'desavanjeId': this.desavanjeId,
'desavanjeName': this.desavanjeName,
};
}
/// Now your method takes a dynamic type which extends your base class Model
/// And you are passing a createCallback parameter which is a Function taking
/// a Map<String, dynamic> as its single parameter and returns an object
/// of type T it will be your method fromJson.
Future<APIResponseModel> get<T extends Model>(
Uri uri, T Function(Map<String, dynamic>) createCallback) async {
APIResponseModel apiRespone = APIResponseModel();
try {
Response response = await _client.get(uri);
final data = jsonDecode(response.body) as Map<String, dynamic>;
apiRespone.addData(createCallback(data));
} catch (e) {
print(e);
}
return apiRespone;
}
Now you should be able to make a call like this:
get<EventModel>(Uri('...'), EventModel.fromJson);
Try the full code on DartPad

Freezed - creating subclasses

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

JsonConverter for unions generated by freezed package in dart

I'm trying to implement toJson/fromJson for a union generated by the freezed package. Let's say we have a class as following:
#freezed
abstract class Result with _$Result {
const factory Result.error(String message) = Error;
const factory Result.success() = Success;
factory Result.fromJson(Map<String, dynamic> json) => _$ResultFromJson(json);
}
Where I want to/fromJson to behave as following:
Result error = Result.error('some error');
expect(error.toJson(), {'type': 'error', 'message': 'some error'});
expect(Result.fromJson({'type': 'error', 'message': 'some error'}), error);
It's stated in the docs that you can use a JsonConverter (fromJSON with multiple classes) but I don't know how to use it properly.
class ResultConverter implements JsonConverter<Result, Map<String, dynamic>> {
const ResultConverter();
#override
Result fromJson(Map<String, dynamic> json) {
if (json == null) {
return null;
}
switch (json['type'] as String) {
case 'success':
return Success.fromJson(json);
case 'error':
return Error.fromJson(json);
default:
throw FallThroughError();
}
}
#override
Map<String, dynamic> toJson(Result object) => object.map(
error: (e) => {'type': 'error', ...e.toJson()},
success: (s) => {'type': 'success', ...s.toJson()},
);
}
fromJson works fine if the factory method calls ResultConverter().fromJson(this) instead of the generated one, but that feels like a workaround and will not work on toJson.
Is it possible to annotate the Result class somehow so that the codegeneration will use the converter?
Yes, this resource has helped me - link to achieve it.
Plus, it works for dedicated named constructor in case of freezed package.
Like this (please note no abstract keyword and private constructor added):
#freezed
class Result with _$Result {
const Result._();
#ResultConverter()
const factory Result.error(String message) = Error;
#ResultConverter()
const factory Result.success() = Success;
factory Result.fromJson(Map<String, dynamic> json) => _$ResultFromJson(json);
}
change toJson(Result object) method to be like that
#override
Map<String, dynamic> toJson(Result object) => object.toJson();
You can just use your converter.
Try this:
final result = ResultConverter().fromJson(json);