JsonConverter for unions generated by freezed package in dart - flutter

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

Related

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.

flutter freezed : equals type of object is not same

I am using freezed to make object from json :
#freezed
class UserMessagesResponseModel with _$UserMessagesResponseModel {
const factory UserMessagesResponseModel({
final Data? data,
}) = _UserMessagesResponseModel;
factory UserMessagesResponseModel.fromJson(final Map<String, dynamic> json) =>
_$UserMessagesResponseModelFromJson(json);
}
#freezed
class Data with _$Data {
const factory Data({
final Messages? messages,
}) = _Data;
factory Data.fromJson(final Map<String, dynamic> json) =>
_$DataFromJson(json);
}...
Now I am trying to make a test and chack type of created object:
test('should return a valid model', () async {
final jsonMap =
json.decode(fixture('message.json')) as Map<String, dynamic>;
final result = UserMessagesResponseModel.fromJson(
jsonMap,
);
expect(result, equals(UserMessagesResponseModel));
});
why I got error:
Expected: Type:<UserMessagesResponseModel>
Actual: _$_UserMessagesResponseModel:<UserMessagesResponseModel...
Freezed Not make same type ? How can I check type ?
I also used:
expect(result, isA<UserMessagesResponseModel>);
but I got this:
Expected: <Closure: () => TypeMatcher<UserMessagesResponseModel> from Function 'isA': static.>
Actual: _$_UserMessagesResponseModel:<UserMessagesResponseModel
The first example
expect(result, equals(UserMessagesResponseModel));
doesn't work because result is an instance and UserMessagesResponseModel is a type, you should use isA.
But on the second example I use isA and it still doesn't work!
You missed the () after isA, I believe that is why it does't work.
from this:
expect(result, isA<UserMessagesResponseModel>);
to this:
expect(result, isA<UserMessagesResponseModel>());

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

How to use Class in variable?

I want to convert fetched data as below, but I got error and emulator shutdown!
What can I do?
Map<String, dynamic> responseClassMap = {
'$ResponseCompany': ResponseCompany,//ResponseCompany is class
'$ResponseCompanyDetail': ResponseCompanyDetail, //ResponseCompanyDetail is class
};
for (var item in responseClassMap.entries) {
if (className == item.key) {
result = responseData.map((data) => item.value.fromJson(data)).toList();
}
}
Here is class ResponseCompany.dart
#JsonSerializable()
class ResponseCompany {
final num sales, ...;
...
factory ResponseCompany.fromJson(Map<String, dynamic> json) => _$ResponseCompanyFromJson(json);
...
Here is ResponseCompany.g.dart
ResponseCompany _$ResponseCompanyFromJson(Map<String, dynamic> json) {
return ResponseCompany(
);
...
}
IMHO item.value.fromJson will not work. Since fromJson is a factory constructor, and in dart's rule, one cannot call factory constructor for a type stored in a variable. (Indeed, the problem is hidden because you create a Map<string, dynamic> and dart allow everything to be called on dynamic at compile time.)
For your specific case, you can do
Map<String, dynamic> map = {
'$ResponseCompany': (d)=>ResponseCompany.fromJson(d),//ResponseCompany is class
'$ResponseCompanyDetail': (d)=>ResponseCompanyDetail.fromJson(d), //ResponseCompanyDetail is class
};
for (var item in map.entries) {
if (className == item.key) {
result = responseData.map((data) => item.value(data)).toList();
}
}

How to deserialize Firestore document with its id using json_serializable in Flutter?

I have a simple Message document in my Firestore database that has some fields.
I use json_serializable to deserialize it to object. My class looks like follows:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'message_firestore.g.dart';
#JsonSerializable(nullable: true, explicitToJson: true)
class MessageFirestore extends Equatable {
MessageFirestore(
this.id, this.content, this.conversationId, this.senderId, this.dateSent);
factory MessageFirestore.fromJson(Map<String, dynamic> json) =>
_$MessageFirestoreFromJson(json);
Map<String, dynamic> toJson() => _$MessageFirestoreToJson(this);
#JsonKey(name: 'Id')
final String id;
#JsonKey(name: 'Content')
final String content;
#JsonKey(name: 'ConversationId')
final String conversationId;
#JsonKey(name: 'SenderId')
final String senderId;
#JsonKey(name: 'DateSent', fromJson: _fromJson, toJson: _toJson)
final DateTime dateSent;
static DateTime _fromJson(Timestamp val) =>
DateTime.fromMillisecondsSinceEpoch(val.millisecondsSinceEpoch);
static Timestamp _toJson(DateTime time) =>
Timestamp.fromMillisecondsSinceEpoch(time.millisecondsSinceEpoch);
}
There is no field called Id in the document, so currently its id is not being deserialized.
However, the key of the map retrieved from Firestore is its id, so this value can be read by manually deserializing the map.
I wish to have access to the id of the document (_b03002...) during deserialization.
Is there any way to configure json_serializable to read this id and store it in id property?
You could modify the fromJson constructor, so that you'd provide the id on the first parameter.
factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
return _$MessageFirestoreFromJson(json)..id = id;
}
Then, from your caller, it'll be something like this
Message(snapshot.documentID, snapshot.data)
You could add another factory into MessageFirestore class.
factory MessageFirestore.fromFire(DocumentSnapshot doc) =>
_$MessageFirestoreFromFire(doc);
after that you will have two factory function in you class.
factory MessageFirestore.fromFire(DocumentSnapshot doc) //...
factory MessageFirestore.fromJson(Map<String, dynamic> json) //...
and add _$MessageFirestoreFromFire(doc) function with copying _$MessageFirestoreFromJson(json) function into message_firestore.g.dart file and edit it like this:
MessageFirestore _$MessageFirestoreFromFire(DocumentSnapshot doc) {
return MessageFirestore(
id: doc.documentID,
content: doc.data['name'] as String,
// ... other parameters
);
}
and in you service to reading the documents you can:
Stream<List<MessageFirestore>> getMessageList() {
return Firestore.instance
.collection('YourCollectionPath')
.snapshots()
.map((snapShot) => snapShot.documents
.map(
(document) => MessageFirestore.fromFire(document),
)
.toList());
}
Easy Peasy
And also this method doesn't interference with other classes that use MessageFirestore instance.
Have a nice day and wish this method works for you. :)
Improvement to Yaobin Then's post: Also remove id on toJson:
factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
return _$MessageFirestoreFromJson(json)..id = id;
}
Map<String, dynamic> toJson() {
var json = _$MessageFirestoreToJson(this);
json.removeWhere((key, value) => key == 'id');
return json;
}
using Yaobin Then's answer, we can improve it forward like this:
factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
return _$MessageFirestoreFromJson(json)..id = id;
}
factory MessageFirestore.fromFire(QueryDocumentSnapshot snapshot) {
return MessageFirestore.fromJson(snapshot.id, snapshot.data() as Map<String, dynamic>);
}
Then, from your caller, it'll be something like this
return StreamBuilder<QuerySnapshot>(
stream: ...,
builder: (context, snp) {
final products = snp.data?.docs;
if (products?.isNotEmpty != true) {
return const Center(
child: Text('No products'),
);
}
final prods = products!.map(
(prod) {
return MessageFirestore.fromFire(prod);
},
).toList();
this way you do not have to fill the 2 arguments in every call, the fromFire factory will handle it for you