Dart null safety and parsing JSON to models with json_serializable - dart

I have a very general question after almost one week of playing around with various tools to de-serialize JSON to Dart and being very frustrated about null safety. The json_serializable package which generates .fromJson and .toJson messages should care about NULL values. But I could not make that work at all! So the one million dollar question is:
Do I have to declare ALL properties of a model as NULLABLE (with a ? e.g. String? myString) if there is any chance that the property may be missing or NULL in the returned JSON ??
When I do NOT declare such members as NULLABLE, I always get this error:
Unhandled Exception: type 'Null' is not a subtype of type 'List<dynamic>' in type cast
These errors always occur in the generated FromJson methods. Here is a sample JSON which causes the problem:
"responseStatus": {
"errorCode": "500",
"message": "This is a test message",
"errors": [
{
"errorCode": "300",
"fieldName": "a field name",
"message": "a message",
"meta": {
"a key": "a value"
}
}
],
"meta": {
"some field": "some value"
}
}
The errors array contains an array of error objects which itself may or may not contain an array of key value pairs (meta). If there are NO errors, the errors array is not submitted as shown here:
"responseStatus": {
"errorCode": "500",
"message": "This is a test message",
"meta": {
"some field": "some value"
}
In such a case the generated DART code crashes as shown below:
ResponseStatus _$ResponseStatusFromJson(Map<String, dynamic> json) =>
ResponseStatus(
errorCode: json['errorCode'] as String? ?? '',
message: json['message'] as String? ?? '',
stackTrace: json['stackTrace'] as String? ?? '',
)
..errors = (json['errors'] as List<dynamic>) // <<==== This is crashing if errors is not in the returned JSON
.map((e) => ResponseError.fromJson(e as Map<String, dynamic>))
.toList()
..meta = Map<String, String>.from(json['meta'] as Map);
The model class I created looks as follows:
#JsonSerializable(explicitToJson: true)
class ResponseStatus extends Equatable {
late final String errorCode;
late final String message;
late final String stackTrace;
late final List<ResponseError> errors;
late final Map<String, String> meta;
ResponseStatus({this.errorCode = '', this.message = '', this.stackTrace = ''}) {
this.errors = [];
this.meta = Map();
}
factory ResponseStatus.fromJson(Map<String, dynamic> json) =>
_$ResponseStatusFromJson(json);
Map<String, dynamic> toJson() => _$ResponseStatusToJson(this);
#override
List<Object?> get props => [errorCode, message, stackTrace, errors, meta];
}
I created a default constructor which initializes all the members (which are NON-nullable) with a value.
If I have to declare all model members as nullable this would be unusable since in my flutter widgets I would have to write thousands of if statements to check if a member is null. I think to avoid this was ONE of the ideas behind sound null safety.
I am curious to learn if this is a bug or how I get this thing working!

This is expected behaviour. If data is not given, a crash must occur, because a non-nullable variable can never have a null value.
You can, however, use a custom deserialisation function to handle this case:
#JsonKey(fromJson: optionalErrorListFromJson)
late final List<ResponseError> errors;
List<ResponseError> optionalErrorListFromJson(List<dynamic>? errors) {
if (errors == null) return const [];
return errors
.map((e) => ResponseError.fromJson(e as Map<String, dynamic>))
.toList(growable: false)
}

Related

Access variables of classes when looping list of classes and getting an instance of Type

I have this Python code:
def from_json(cls, data: dict, api: Optional[Overpass] = None) -> "Result":
"""
Create a new instance and load data from json object.
:param data: JSON data returned by the Overpass API
:param api:
:return: New instance of Result object
"""
result = cls(api=api)
elem_cls: Type[Union["Area", "Node", "Relation", "Way"]]
for elem_cls in [Node, Way, Relation, Area]:
for element in data.get("elements", []):
e_type = element.get("type")
if hasattr(e_type, "lower") and e_type.lower() == elem_cls._type_value:
result.append(elem_cls.from_json(element, result=result))
return result
and I want to convert this to Dart code. This is what I've come up with:
factory Result.fromJson(Map<String, dynamic> data, {Overpass? api}) {
Result result = Result(api: api!);
Type elemCls;
for (elemCls in [Node, Way, Relation, Area]) {
for (Map<String, dynamic> element in data["elements"]) {
String eType = element["type"];
if (eType.toLowerCase() == elemCls.typeValue) {
result.append(elemCls.fromJson(element, result: result));
}
}
}
return result;
}
My problems are The getter '_typeValue' isn't defined for the type 'Type'. and The method 'fromJson' isn't defined for the type 'Type'.. This is because _typeValue and fromJson belong to the classes Node, Way, Relation and Area and not to Type which is what I get with this for loop. What do I have to change to get this working like in Python?
You could do eType.toLowerCase() == elemCls.toString().toLowerCase(). However, elem_cls.from_json(element, result=result) cannot be directly done in Dart. There is no way to generically instantiate an object with type elemCls.
What you instead could do is create a lookup table to map type names to constructors:
final _constructionTable = {
"node": Node.fromJson,
"way": Way.fromJson,
"relation": Relation.fromJson,
"area": Area.fromJson,
};
factory Result.fromJson(Map<String, dynamic> data, {Overpass? api}) {
Result result = Result(api: api!);
for (Map<String, dynamic> element in data["elements"]) {
String eType = element["type"]!;
var fromJson = _constructionTable[eType.toLowerCase()];
if (fromJson != null) {
result.append(fromJson(element, result: result));
}
}
return result;
}
Note that this also would be a bit more efficient too, and it avoids relying on the specific strings generated for the Dart types.

Dart: Maps nested in maps

I want to store various data for my app in a single place, in a map. In JS, I'd store in a JSON file, and I want to use the same sort of approach, but struggling with Dart. I can't seem to work with nested lists or maps.
Here's essentially what I want to do:
var items = {
"item1": {
"message" : "aa",
"nested1": {
"message": "bb",
"nested2" : {
"message" : "cc"
},
}
},
};
void main() {
var message1 = items["item1"]?["message"];
print(message1);
print(message1.runtimeType);
var message2 = items["item1"]?["nested1"]?["message"];
print(message2);
print(message2.runtimeType);
var message3 = items["item1"]?["nested1"]?["nested2"]?["message"];
print(message3);
print(message3.runtimeType);
}
I've been struggling to make this work in Dartpad.
message1 works as expected, but then I can't seem to work my way down the tree...
Is this a shortcoming with map literals? Do I need to use constructors? Or am I missing something bigger?
Your problem is that items is inferred to be of type Map<String, Map<String, Object>>, but Object does not have an operator []. Therefore when you eventually extract that Object, you will not be able to do anything with it until you cast it a more specific type.
What you probably want instead is to explicitly declare items as Map<String, dynamic> to disable static type-checking on the Map's values:
var items = <String, dynamic>{
"item1": ...
};
Of course, when you disable static type-checking, you are responsible for ensuring that the values you get from the Map are what you expect, or you will get NoSuchMethod or TypeError exceptions at runtime. If you do want static type-checking, you should use define custom classes instead of using a blob of key-value properties.

How to convert list to map in dart?

I'm new to dart. I'm trying to covert my Holiday class to Map to be used in my calendar. I tried using Map.fromIterable but it only convert it to <String, dynamic>?
class Occasion {
final List<Holiday> holidays;
Map<DateTime, List> toMap() {
var map = Map.fromIterable(holidays,
key: (e) => DateFormat('y-M-d').format(DateTime.parse(e.date)),
value: (e) => e.name);
print(map);
}
}
class Holiday {
final String date;
final String name;
Holiday({
this.date,
this.name,
});
factory Holiday.fromJson(Map<String, dynamic> parsedJson) {
return Holiday(date: parsedJson['date'], name: parsedJson['name']);
}
}
There are two things:
First: The type parameters of your returned map aren't right for the values you produce in fromIterable. You say it should be List, but in value: ... you are only producing a single String.
Secondly, as I said in my comment you need to help out the dart compiler here a little bit. The compiler isn't very smart. It doesn't see that you are only producing Strings in value. You need to tell him that.
To be fair. This might not be the problem of the compiler, but an overuse of the dynamic type in the collections library.
Map<String, String> toMap() {
var map = Map<String, String>.fromIterable(holidays,
key: (e) => e.date,
value: (e) => e.name );
return map;
}
Just remember: be precise with your types. If you run into type errors start putting additional type information everywhere you can. If you feel it's to cluttered after that, try removing them one spot at a time and see where it leads you.

flutter: Need either specifiedType or discriminator field

everyone,i'm confuse on this problem few days,any one can help me?
when i using flutter redux,i got this problems.
Deserializing '[data, {accounts: [], version: 5.4}, null, null]' to 'LoginResponse' failed due to: Invalid
argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.
here is LoginResponse Deserialize method:
LoginResponse deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType: FullType.unspecified}) {
final result = new LoginResponseBuilder();
final iterator = serialized.iterator;
while (iterator.moveNext()) {
final key = iterator.current as String;
iterator.moveNext();
final dynamic value = iterator.current;
switch (key) {
case 'data':
result.data.replace(serializers.deserialize(value, specifiedType: const FullType(LoginResponseData)) as LoginResponseData);
break;
case 'error':
result.error.replace(serializers.deserialize(value,
specifiedType: const FullType(ErrorMessage)) as ErrorMessage);
break;
}
}
return result.build();
}
abstract class LoginResponseData implements Built<LoginResponseData, LoginResponseDataBuilder> {
factory LoginResponseData([void updates(LoginResponseDataBuilder b)]) = _$LoginResponseData;
LoginResponseData._();
BuiltList<CompanyEntity> get accounts;
String get version;
static Serializer<LoginResponseData> get serializer => _$loginResponseDataSerializer;
}
What really hurts me is that the debugging mode of android studio seems some problem? some variable always shows "Collecting data... it's hard to me to fix this problem cause i can't got some key variable's value.just like:specifiedType.
There is some screenshot when debuging.
i'm great appreciate if any one could give me some tips or answers!!! thanks!!!!

Dart Built Value Deserialize List of Objects

I have an API that's returning a list of objects...
[{}, {}, {}, ...]
I already have a defined and working built_value model for each object. However, now I need to deserialize the list.
I currently am trying something like this:
List<Map<String, dynamic>> json = JSON.decode(DEMO_TASK);
json.expand<Task>((Map<String, dynamic> map) => _serializers.deserializeWith<Task>(Task.serializer, map));
However, that causes issues since it says _serializers.deserializeWith return type Task isn't an Iterable<Task> as defined by the closure.
How do I go about deserializing the list. I'm sure I'm missing something super basic.
In case you want to have more general approach you can use this code snippet:
In case anybody needs this functionality I leave here code snippet of how to handle this situation (code should be placed in serializers.dart file):
Serializers standardSerializers = (serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
T deserialize<T>(dynamic value) =>
standardSerializers.deserializeWith<T>(standardSerializers.serializerForType(T), value);
BuiltList<T> deserializeListOf<T>(dynamic value) =>
BuiltList.from(value.map((value) => deserialize<T>(value)).toList(growable: false));
So if you have json file
[
{
"name": "test1",
"result": "success"
},
{
"name": "test2",
"result": "fail"
}
]
And built value class:
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
part 'test_class.g.dart';
abstract class TestClass implements Built<TestClass, TestClassBuilder> {
String get name;
String get result;
TestClass._();
factory TestClass([updates(TestClassBuilder b)]) = _$TestClass;
static Serializer<TestClass> get serializer => _$testClassSerializer;
}
You can use method deserializeListOf from above as:
import 'package:path_to_your_serializers_file/serializers.dart';
final BuiltList<TestClass> listOfTestClasses = deserializeListOf<TestClass>(json);
Yup. I missed something basic. I was thinking I was using a stream, but since it's a list you just have to use the .map function on a list.
List<Map<String, dynamic>> json = JSON.decode(DEMO_TASK);
List<Task> tasks = json.map<Task>((Map<String, dynamic> map) => _serializers.deserializeWith<Task>(Task.serializer, map)).toList();

Resources