Dart SDK version: 2.12.0 (stable) (Thu Feb 25 19:50:53 2021 +0100) on "linux_x64"
4.0.2When creating a custom JsonConverter that handles nullable types, it will not be applied to non-nullables. Example, consider this:
import 'package:json_annotation/json_annotation.dart';
part 'mydata.g.dart';
class ForceUtcDateTime implements JsonConverter<DateTime?, String?> {
const ForceUtcDateTime();
@override
DateTime? fromJson(String? json) => json == null
? null
: DateTime.parse('$json${json.endsWith('Z') ? '' : 'Z'}');
@override
String? toJson(DateTime? json) => json?.toIso8601String();
}
@JsonSerializable()
@ForceUtcDateTime()
class MyData {
final DateTime created;
final DateTime? updated;
const MyData({required this.created, this.updated});
factory MyData.fromJson(Map<String, dynamic> json) => _$MyDataFromJson(json);
Map<String, dynamic> toJson() => _$MyDataToJson(this);
}
A custom DateTime converter is created that handles DateTime?. The generated code is as follows:
MyData _$MyDataFromJson(Map<String, dynamic> json) {
return MyData(
created: DateTime.parse(json['created'] as String),
updated: const ForceUtcDateTime().fromJson(json['updated'] as String?),
);
}
Map<String, dynamic> _$MyDataToJson(MyData instance) => <String, dynamic>{
'created': instance.created.toIso8601String(),
'updated': const ForceUtcDateTime().toJson(instance.updated),
};
I would expect both created and updated to be handled by my ForceUtcDateTime converted, not just updated
If this is intended, I would like to know what is the expected way of handling such cases.
Having same issue, also impacts when the type is used in a list or anything
// my_data.dart
@JsonSerializable()
@MyTypeConverter()
class MyData {
late MyType nonNullable;
MyType? yesNullable;
List<MyType>? someList;
const MyData();
factory MyData.fromJson(Map<String, dynamic> json) => _$MyDataFromJson(json);
Map<String, dynamic> toJson() => _$MyDataToJson(this);
}
// my_data.g.dart
MyData _$MyDataFromJson(Map<String, dynamic> json) {
return MyData()
..nonNullable = const MyTypeConverter().fromJson(json['nonNullable'] as String)
..yesNullable = const MyTypeConverter().fromJson(json['nonNullable'] as String)
..someList = (json['someList'] as List<dynamic>?)
?.map((e) => const MyTypeConverter().fromJson(e as String))
.toList();
}
Map<String, dynamic> _$MyDataToJson(MyData instance) => <String, dynamic>{
'nonNullable': const MyTypeConverter().toJson(instance.nonNullable),
'yesNullable': const MyTypeConverter().toJson(instance.yesNullable),
'someList': instance.someList
?.map(const MyTypeConverter().toJson)
.toList(),
};
I end up with two errors:
The argument type 'MyType?' can't be assigned to the parameter type 'MyType'. (my_data.g.dart:4)
A value of type 'List<MyType?>?' can't be assigned to a variable of type 'List<MyType>?'. (my_data.g.dart:6)
The short fix is to create another converter that handles the nullable case.
we could set it up so that we handle nullable values with non-nullable converters like we do for DateTime, etc. And just pass them through.
I'll need to think about that case.
Here is why I think this use-case should be supported: consider a modified example from my initial comment:
import 'package:json_annotation/json_annotation.dart';
part 'asd.g.dart';
class MyType {
final int val;
const MyType(this.val);
factory MyType.fromJson(String json) => MyType(int.parse(json));
String toJson() => val.toString();
}
@JsonSerializable()
class MyData {
final MyType created;
final MyType? updated;
const MyData({required this.created, this.updated});
factory MyData.fromJson(Map<String, dynamic> json) => _$MyDataFromJson(json);
Map<String, dynamic> toJson() => _$MyDataToJson(this);
}
The only real difference is using a custom type with fromJson and toJson methods. This example correctly serializes and handles both MyType and MyType?. So if this works, I think the converter example should also work.
On a similar note, perhaps there is a better way of creating custom serializations for existing types? Because that is what I am using a Converter for, but it's tedious to decorate all of my classes with this converter.
With defaultValue, generated code is redundant.
@JsonSerializable()
@SampleConverter()
class Sample {
@JsonKey(defaultValue: 1)
final int val;
const Sample({this.val = 1});
factory Sample.fromJson(Map<String, dynamic> json) => _$SampleFromJson(json);
}
class SampleConverter implements JsonConverter<int, String?> {
const SampleConverter();
@override
int fromJson(String? json) => json == null ? 1 : int.parse(json);
@override
String? toJson(int object) => "$object";
}
Sample _$SampleFromJson(Map<String, dynamic> json) {
return Sample(
val: const SampleConverter().fromJson(json['val'] as String?) ?? 1, // warning: The left operand can't be null
);
}
So, I want to use not JsonConverter<int, String?> but JsonConverter<int?, String?>.
Same for JsonKey's fromJson/toJson.
The following code doesn't work:
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart = 2.12
import 'package:json_annotation/json_annotation.dart';
part 'myclass.g.dart';
String _fromJson(String _) => '';
String _toJson(String _) => '';
@JsonSerializable(explicitToJson: true)
class MyClass extends JsonSerializable {
MyClass();
factory MyClass.fromJson(Map<String, dynamic> json) =>
_$MyClassFromJson(json);
@JsonKey(fromJson: _fromJson, toJson: _toJson)
late String str;
@JsonKey(fromJson: _fromJson, toJson: _toJson)
String? strNullable;
Map<String, dynamic> toJson() => _$MyClassToJson(this);
}
But, thinking about it, maybe it _should_ work, and yield something similar to the following? (considering nullability before running the fn)
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart=2.12
part of 'myclass.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MyClass _$MyClassFromJson(Map<String, dynamic> json) {
return MyClass()
..str = _fromJson(json['str'] as String)
..strNullable = json['strNullable'] == null
? null
: _fromJson(json['strNullable'] as String);
}
Map<String, dynamic> _$MyClassToJson(MyClass instance) => <String, dynamic>{
'str': _toJson(instance.str),
'strNullable': instance.strNullable == null
? null
: _toJson(instance.strNullable as String),
};
I just did some work here in https://github.com/google/json_serializable.dart/pull/873
Let me try to pull it over...
Most helpful comment
I just did some work here in https://github.com/google/json_serializable.dart/pull/873
Let me try to pull it over...