Custom json converter with freezed without a wrapper class - flutter

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.

Related

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

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

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

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

How to include factory method into abstract class?

Is there any way to include factory method into abstract class? I've got a lot of model classes which I use to populate data. They all have same methods in them fromMap and toMap where fromMap is an factory method
class MyModelClass implements Tools {
//toMap
Map<String, dynamic> toMap() {
return {
...
};
}
//fromMap
factory MyModelClass.fromMap(
Map<String, dynamic> data) {
...
return MyModelClass(...)}
}
I can easily include toMap method but trouble with factory one
abstract class Tools {
Map<String, dynamic> toMap();
///here the factory method
}
Reason to put these methods in to abstract class is to be able to use the abstract class only in my database class to retrieve and send data.
An idea what comes to my mind
abstract class Tools {
Map<String, dynamic> toMap();
factory Tools.fromMap(Map<String, dynamic> data, String sw) {
if (sw == 'a')
return MyModelClass1();
else
return MyModelClass2();
}
}
Your factory constructor must return an instance of MyModelClass, but that's abstract and cannot be instantiated. It can instantiate and return a (possibly private) concrete, derived class. That is what Map's constructor does. (Map is an abstract class with factory constructors.)