Flutter Parse JSON to Model with fromJson Function Always Fails - flutter

I am very confused about this problem, parsing JSON to model always fails with the message "null is not subtype of type string in type cast". I've made sure all values are not null. When manually initiating the model with the constructor, it works, but using the fromJson function always fails.
This is my model:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_model.g.dart';
part 'user_model.freezed.dart';
#freezed
class UserModel with _$UserModel {
const factory UserModel({
required String id,
required String identity,
required String name,
required String email,
required String phone,
required String role,
required String? createdAt,
required String? updatedAt,
required String? accountVerifiedAt,
required String jsonWebToken,
}) = _UserModel;
const UserModel._();
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
This is how I parse with fromJson, but it always throw exception (null is not a subtype of type string in type cast):
var user = UserModel.fromJson(object['data']);
But it works:
var user = UserModel(
id: object['data']['id'],
identity: object['data']['identity'],
name: object['data']['name'],
email: object['data']['email'],
phone: object['data']['phone'],
role: object['data']['role'],
createdAt: object['data']['createdAt'],
updatedAt: object['data']['updatedAt'],
accountVerifiedAt: object['data']['accountVerifiedAt'],
jsonWebToken: object['data']['jsonWebToken'],
);

Maybe your json has null value that you don't recognized. It happens to me all the times. By the way, I never use freezed_annotation because model is easy to write by hands, plus if there is is a bug, it would be easier to fix. You can do like this:
When back-end responds data:
if (json["error_code"] != 0) { // Back-end responds an error
// handle error
} else {
YourModel.fromJson(json);
}
And here is your model:
class YourModel {
String variable;
YourModel(this.variable = <default value>);
YourModel.fromJson(Map<String, dynamic> json) {
this.variable = json["blah"]["yolo"] ?? <value if json null>;
}
}

I just figured out, that it's caused by the generated freezed class, my JSON response use camelCase as keys, but the model generates with snake_case. So basically I convert my response keys from camelCase to snake_case before initiating the object using the fromJson function.
Map<String, dynamic> data = {};
for (var element in Map<String, dynamic>.from(object['data']).entries) {
data[StringUtils.camelCaseToLowerUnderscore(element.key)] =
element.value;
}
object['data'] = UserModel.fromJson(data);
If anyone facing the same problem, I hope it can help. And if anyone has a better solution, you guys can post it here too. Thank you.
NB: I use basic_utils: ^4.4.3
EDIT:
Following this article https://codewithandrea.com/articles/parse-json-dart-codegen-freezed/, we can add #JsonKey annotation to make sure the property name is correct, example:
#JsonKey(name: 'customerId') required String? customerId
Without giving #JsonKey annotation, customerId will be written as customer_id.

Related

Passing in ID for a firestore document converting to a freezed data class (fromJson)

What is the best way to return a document from firebase when the id is not coming from the json firestore returns, using a freezed data class with fromJson/toJson?
I want the id to be required (to make sure I always have one - and plan to use an "uninstantiated ID" for objects that haven't been saved to firestore yet).
But if I mark id as required, but ignore it for json generation, the fromJson doesn't work when I try to do code generation. Is there a way to have code generation work where I specify a second parameter in the fromJson method to pass in id separately?
Here is my base data class:
#freezed
class Habit with _$Habit {
const Habit._();
const factory Habit({
#JsonKey(includeFromJson: false, includeToJson: false) required String id,
required String title,
String? description,
}) = _Habit;
factory Habit.newEmpty({required String userId}) => Habit(
id: uninstantiatedId,
title: '',
);
// Ideally would like to have this take a string
// id in addition to the json, and set the id to
// what is passed in separately.
factory Habit.fromJson(String id, Map<String, dynamic> json) => _$HabitFromJson(json);
}
So I can't do it with the id property set as non-nullable but without a default value. I either need to make id a nullable String? type, or set a #Default('textvalue') for the id.
Then I can add the copyWith method to my fromJson method which seems to work. Updated code below:
#freezed
class Habit with _$Habit {
const Habit._();
const factory Habit({
// I have to add a default value, as pictured below, or set this to
// String? so it is optional. Otherwise the json_serializable code
// will not be generated.
#JsonKey(includeFromJson: false, includeToJson: false) #Default(uninstantiatedId) String id,
required String title,
String? description,
}) = _Habit;
factory Habit.newEmpty({required String userId}) => Habit(
id: uninstantiatedId,
title: '',
);
// I then add the copyWith to my fromJson portion to add the id
factory Habit.fromJson(String id, Map<String, dynamic> json) => _$HabitFromJson(json).copyWith(id: id);
}

Why this variable is type 'String?' even when any null check was removed?

I want to store a token with the dart package shared_preferences(just for testing purposes), and dart retrofit to make the http requets, but when using _testToken.setString(key, token), dart pops an error at the token (_testToken.setString(key, token)): The argument type 'String?' can't be assigned to the parameter type 'String', I removed any null safe from the retrofit token response looking for a solution but I am getting the same error, and besides that dart is showing another error saying that I should add null safe to the token response which will give an error if I use it to store shared_preferences.
Here the retrofit Response file:
#JsonSerializable()
class ResponseData {
#JsonKey(name: 'status')
final int? statusCode;
Map<String, String> data;
ResponseData({this.data, this.statusCode}); // Here I had required on data and removed it
factory ResponseData.fromJson(Map<String, dynamic> json) => _$ResponseDataFromJson(json);
Map<String, dynamic> toJson() => _$ResponseDataToJson(this);
}
Main Retrofit http file:
part 'rest_api_service.g.dart';
#RestApi(baseUrl: "http://192.168.1.120/api/mobile")
abstract class RestClient {
factory RestClient(Dio dio, {String? baseUrl}) = _RestClient;
#Headers(<String, dynamic> {
"Cache-Control" : "max-age=0",
"Data-Agent" : "EMMappV2",
"content-type" : "multipart/form-data",
"accept" : "application/json"
})
#POST("/login")
Future<ResponseData> loginUser( // here using the ResponseData
#Field("email") String email,
#Field("password") String password,
#Field("device_name") String deviceName
);
}
AuthProvider file (only showing the function):
await client.loginUser(email, password, deviceName).then(
(value) => {
print(value.data["token"]),
variabl = value.data["token"],
// _storage.write(key: "token", value: value.data["token"]), Does not work on web version
_testToken.setString('token', value.data["token"]), // here the call with the error
if (value.statusCode == 200 ) {
RouteKey.navigatorKey.currentState!.pushNamed(loginRoute)
}
});
The problem is that the Dart static analysis can't know for sure that you have a valid "token" key inside the value.data map with a non-nullable value value.data["token"]
A workaround is to use the ! bang operator to mark this value as non-nullable.
_testToken.setString('token', value.data["token"]!),
The Map index operator is nullable
This isn’t really a change, but more a thing to know. The index []
operator on the Map class returns null if the key isn’t present. This
implies that the return type of that operator must be nullable: V?
instead of V
This is because data['value'] is not guaranted to actually return a value from the map (the key could not exist, and because of Map/HashMap interface, throwing is not a good option) so even if you know all values are not null you don't know what keys will return a not null value at runtime.
Check operators in Map class
If you really want to check your data is not nullable and treat it as that your model should have decode all keys as non nullable parameters
#JsonSerializable()
class ResponseData {
#JsonKey(name: 'status')
final int? statusCode;
final String token;
//final String userName; //and other stuff inside your map
ResponseData({this.token, this.userName, this.statusCode}); // Here I had required on data and removed it
factory ResponseData.fromJson(Map<String, dynamic> json) => _$ResponseDataFromJson(json);
Map<String, dynamic> toJson() => _$ResponseDataToJson(this);
}
Now your Response data will actually holds all parameters as not null and Dart will not check it for you (unless the map actually doesn't have those values, then fromJson will throw and you will get why Map is actually not a recommendable way to check for not null)

Can we use json_serializable package to serialize and deserialize user models?

After generating part 'UserModel.g.dart'; successfully, I got the following error upon trying to do
if (user != null) {
var uid = user.providerData.first.uid;
var displayName = user.providerData.first.displayName;
var email = user.providerData.first.email;
var phoneNumber = user.providerData.first.phoneNumber;
var providerId = user.providerData.first.providerId;
var photoUrl = user.providerData.first.photoURL;
UserModel userModel = UserModel(phoneNumber,
uid: uid,
displayName: displayName,
email: email,
providerId: providerId,
photoUrl: photoUrl);
Query query = users.where('uid', isEqualTo: uid);
query.get().then((querySnapshot) => {
if (querySnapshot.size < 1) {addUser(userModel)}
});
}
}
And here is my UserModel.dart without any errors in the file.
import 'package:json_annotation/json_annotation.dart';
part 'UserModel.g.dart';
#JsonSerializable()
class UserModel {
late String? uid;
late String? displayName;
late String? email;
late String? phoneNumber;
late String providerId;
late String? photoUrl;
UserModel(this.phoneNumber, {required this.uid, required this.displayName, required this.email,
required this.providerId, required this.photoUrl});
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
}
Offcourse, I could manually map each value. But as recommended here https://flutter.dev/docs/development/data-and-backend/json, to prevent
Manual decoding does not perform well when your project becomes bigger. Writing decoding logic by hand can become hard to manage and error-prone. If you have a typo when accessing a nonexistent JSON field, your code throws an error during runtime.
I used the plugin json_serializable to sort of automate it.
But I got the following error:
Expected a value of type 'Map<String, dynamic>', but got one of type 'UserModel$'
Is this error expected due to incompatibility or am I doing something wrong? Please respond. Thank you very much. :)
I'm guessing this is where your issue is.
query.get().then((querySnapshot) => {
if (querySnapshot.size < 1) {addUser(userModel)}
});
Use the toJson method you created in your model class to pass in a Map instead of your custom UserModel object.
{addUser(userModel.toJson())}

Flutter: Merge objects of same type and combine defined properties (like javascript?)

Using Flutter and Dart, lets say I have this class:
#JsonSerializable()
class User {
#JsonKey(nullable: true, required: false)
final String name;
#JsonKey(nullable: true, required: false)
final int age;
User({
this.name,
this.age,
});
factory User.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
#override
String toString() {
return 'User ${toJson().toString()}';
}
}
In my code I'm trying to create a new instance of this class to be sent to /update-user endpoint on the server. My goal is to send an object that contains just the properties I would like the server to update. let's say only update age.
Using
final dto = new UpdateUserRequest(
age: 34
);
results in this json representation: {name: null, age: 34}, which will override the already existing name on the server.
I also tried 'json merging' 2 objects, the one I already have with the name, and the new dto that updates age:
final combined = UpdateUserRequest.fromJson({
...(dtoWithAge.toJson()),
...(existingUserWithName.toJson()),
});
but no matter how I play around with these, they end up overriding each other.
So, is there anyway to get a json/DTO instance of the class, that only contains the properties and values I want the server to update? (trying to achieve something very similar to javascript)
I don't think there's a pre-implemented solution for that. Assuming I understood what you're trying to accomplish, how about adding a copyWith method to User?
User copyWith({
String name,
int age,
}) => User(
name: name ?? this.name,
age: age ?? this.age,
);
You'd use it like this:
final existingUser = User(name: 'John');
final updatedUser = existingUser.copyWith(age: 25);
sendUpdateRequestWith(updatedUser);
For you convenience, there's a plugin to generate it.
vscode: https://marketplace.visualstudio.com/items?itemName=BendixMa.dart-data-class-generator
IDEA/Android Studio: https://plugins.jetbrains.com/plugin/12429-dart-data-class

How to parse json data between json with uppercase to class in Flutter

From PHP server, I receive a json data
and using class in Flutter I want to transfer the data to class.
But because uppercase or hyphen character I can't parse from json data to class type.
Maybe that is inconsistency problem between php json data and class.
But I can't use uppercase or hyphen character in class of Flutter.
How to resolve this?
Received data from PHP
`{reset: true, SESSIONID: 230hnoco0lnao6da7gqd9t5i56, chat_nations:.....}
**Defined class in Flutter **
class Post {
String id, sessionid;
bool error, reset;
List<ChatNation> chatNations = [];
Post({
this.reset,
this.sessionid,
List<ChatNation> chatNations,
this.error,
this.id,
});
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}
Result in defined class
{"id":null,"sessionid":null,"error":false,"reset":true,"chatNations":[]}
JSON Returned from PHP service is wrong. SESSIONID is a string, so it must be enclosed in "double quotes"
Dart json_annotation has JsonKey annotation which can take optional name for JSON key. So you can re-write class Post as
#JsonSerializable()
class Post {
#JsonKey(nullable: true)
String id;
#JsonKey(nullable: false, name="SESSIONID")
String sessionid;
....
}
Check the documentation of JsonKey annotation.
However, this approach has a problem: JSON generated by DART will also have these incosistent names/keys.
If you want to fix this, better you write your own implementatino of fromJson to e.g.
Post fromJson(Map json) => Post({
sessionid: (json['sessionid'] ?? json['SESSIONID']) as String,
id: json[id],
....
});