diff --git a/json_serializable/CHANGELOG.md b/json_serializable/CHANGELOG.md index fc5676b04..7e79368d3 100644 --- a/json_serializable/CHANGELOG.md +++ b/json_serializable/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.3.0-dev + +- Added support for using a `JsonConverter` on properties + of type `MyClass?`. ([#822](https://github.com/google/json_serializable.dart/issues/822)) + ## 6.2.0 - Added support for the new `FieldRename.screamingSnake` field in diff --git a/json_serializable/lib/src/type_helpers/json_converter_helper.dart b/json_serializable/lib/src/type_helpers/json_converter_helper.dart index 8c0df9f3a..10fa483b4 100644 --- a/json_serializable/lib/src/type_helpers/json_converter_helper.dart +++ b/json_serializable/lib/src/type_helpers/json_converter_helper.dart @@ -32,6 +32,24 @@ class JsonConverterHelper extends TypeHelper { return null; } + if (!converter.fieldType.isNullableType && targetType.isNullableType) { + const converterToJsonName = r'_$JsonConverterToJson'; + context.addMember(''' +Json? $converterToJsonName( + Value? value, + Json? Function(Value value) toJson, +) => ${ifNullOrElse('value', 'null', 'toJson(value)')}; +'''); + + return _nullableJsonConverterLambdaResult( + converter, + name: converterToJsonName, + targetType: targetType, + expression: expression, + callback: '${converter.accessString}.toJson', + ); + } + return LambdaResult(expression, '${converter.accessString}.toJson'); } @@ -49,6 +67,24 @@ class JsonConverterHelper extends TypeHelper { final asContent = asStatement(converter.jsonType); + if (!converter.jsonType.isNullableType && targetType.isNullableType) { + const converterFromJsonName = r'_$JsonConverterFromJson'; + context.addMember(''' +Value? $converterFromJsonName( + Object? json, + Value? Function(Json json) fromJson, +) => ${ifNullOrElse('json', 'null', 'fromJson(json as Json)')}; +'''); + + return _nullableJsonConverterLambdaResult( + converter, + name: converterFromJsonName, + targetType: targetType, + expression: expression, + callback: '${converter.accessString}.fromJson', + ); + } + return LambdaResult( expression, '${converter.accessString}.fromJson', @@ -57,24 +93,51 @@ class JsonConverterHelper extends TypeHelper { } } +String _nullableJsonConverterLambdaResult( + _JsonConvertData converter, { + required String name, + required DartType targetType, + required String expression, + required String callback, +}) { + final jsonDisplayString = typeToCode(converter.jsonType); + final fieldTypeDisplayString = converter.isGeneric + ? typeToCode(targetType) + : typeToCode(converter.fieldType); + + return '$name<$jsonDisplayString, $fieldTypeDisplayString>(' + '$expression, $callback)'; +} + class _JsonConvertData { final String accessString; final DartType jsonType; + final DartType fieldType; + final bool isGeneric; _JsonConvertData.className( String className, String accessor, this.jsonType, - ) : accessString = 'const $className${_withAccessor(accessor)}()'; + this.fieldType, + ) : accessString = 'const $className${_withAccessor(accessor)}()', + isGeneric = false; _JsonConvertData.genericClass( String className, String genericTypeArg, String accessor, this.jsonType, - ) : accessString = '$className<$genericTypeArg>${_withAccessor(accessor)}()'; + this.fieldType, + ) : accessString = + '$className<$genericTypeArg>${_withAccessor(accessor)}()', + isGeneric = true; - _JsonConvertData.propertyAccess(this.accessString, this.jsonType); + _JsonConvertData.propertyAccess( + this.accessString, + this.jsonType, + this.fieldType, + ) : isGeneric = false; static String _withAccessor(String accessor) => accessor.isEmpty ? '' : '.$accessor'; @@ -127,7 +190,11 @@ _JsonConvertData? _typeConverterFrom( accessString = '${enclosing.name}.$accessString'; } - return _JsonConvertData.propertyAccess(accessString, match.jsonType); + return _JsonConvertData.propertyAccess( + accessString, + match.jsonType, + match.fieldType, + ); } final reviver = ConstantReader(match.annotation).revive(); @@ -145,6 +212,7 @@ _JsonConvertData? _typeConverterFrom( match.genericTypeArg!, reviver.accessor, match.jsonType, + match.fieldType, ); } @@ -152,11 +220,13 @@ _JsonConvertData? _typeConverterFrom( match.annotation.type!.element!.name!, reviver.accessor, match.jsonType, + match.fieldType, ); } class _ConverterMatch { final DartObject annotation; + final DartType fieldType; final DartType jsonType; final ElementAnnotation elementAnnotation; final String? genericTypeArg; @@ -166,6 +236,7 @@ class _ConverterMatch { this.annotation, this.jsonType, this.genericTypeArg, + this.fieldType, ); } @@ -191,9 +262,15 @@ _ConverterMatch? _compatibleMatch( final fieldType = jsonConverterSuper.typeArguments[0]; - if (fieldType == targetType) { + // Allow assigning T to T? + if (fieldType == targetType || fieldType == targetType.promoteNonNullable()) { return _ConverterMatch( - annotation, constantValue, jsonConverterSuper.typeArguments[1], null); + annotation, + constantValue, + jsonConverterSuper.typeArguments[1], + null, + fieldType, + ); } if (fieldType is TypeParameterType && targetType is TypeParameterType) { @@ -212,6 +289,7 @@ _ConverterMatch? _compatibleMatch( constantValue, jsonConverterSuper.typeArguments[1], '${targetType.element.name}${targetType.isNullableType ? '?' : ''}', + fieldType, ); } diff --git a/json_serializable/pubspec.yaml b/json_serializable/pubspec.yaml index 081f91ac1..00db66b4f 100644 --- a/json_serializable/pubspec.yaml +++ b/json_serializable/pubspec.yaml @@ -1,5 +1,5 @@ name: json_serializable -version: 6.2.0 +version: 6.3.0-dev description: >- Automatically generate code for converting to and from JSON by annotating Dart classes. diff --git a/json_serializable/test/generic_files/generic_class.g.dart b/json_serializable/test/generic_files/generic_class.g.dart index e45e840d4..c2f216483 100644 --- a/json_serializable/test/generic_files/generic_class.g.dart +++ b/json_serializable/test/generic_files/generic_class.g.dart @@ -39,10 +39,10 @@ GenericClassWithConverter ..fieldObject = json['fieldObject'] ..fieldDynamic = json['fieldDynamic'] ..fieldInt = json['fieldInt'] as int? - ..fieldT = _SimpleConverter() - .fromJson(json['fieldT'] as Map) - ..fieldS = _SimpleConverter() - .fromJson(json['fieldS'] as Map) + ..fieldT = _$JsonConverterFromJson, T>( + json['fieldT'], _SimpleConverter().fromJson) + ..fieldS = _$JsonConverterFromJson, S>( + json['fieldS'], _SimpleConverter().fromJson) ..duration = const _DurationMillisecondConverter.named() .fromJson(json['duration'] as int?) ..listDuration = const _DurationListMillisecondConverter() @@ -54,14 +54,28 @@ Map _$GenericClassWithConverterToJson( 'fieldObject': instance.fieldObject, 'fieldDynamic': instance.fieldDynamic, 'fieldInt': instance.fieldInt, - 'fieldT': _SimpleConverter().toJson(instance.fieldT), - 'fieldS': _SimpleConverter().toJson(instance.fieldS), + 'fieldT': _$JsonConverterToJson, T>( + instance.fieldT, _SimpleConverter().toJson), + 'fieldS': _$JsonConverterToJson, S>( + instance.fieldS, _SimpleConverter().toJson), 'duration': const _DurationMillisecondConverter.named().toJson(instance.duration), 'listDuration': const _DurationListMillisecondConverter() .toJson(instance.listDuration), }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + Issue980ParentClass _$Issue980ParentClassFromJson(Map json) => Issue980ParentClass( (json['list'] as List) diff --git a/json_serializable/test/json_serializable_test.dart b/json_serializable/test/json_serializable_test.dart index 9d29fce0b..ef5e38e01 100644 --- a/json_serializable/test/json_serializable_test.dart +++ b/json_serializable/test/json_serializable_test.dart @@ -85,6 +85,7 @@ const _expectedAnnotatedTests = { 'JsonConverterCtorParams', 'JsonConverterDuplicateAnnotations', 'JsonConverterNamedCtor', + 'JsonConverterNullableToNonNullable', 'JsonConverterOnGetter', 'JsonConverterWithBadTypeArg', 'JsonValueValid', diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.dart b/json_serializable/test/kitchen_sink/kitchen_sink.dart index 92bd1c94d..4eabab0e2 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.dart @@ -66,9 +66,13 @@ class _Factory implements k.KitchenSinkFactory { [], BigInt.zero, {}, + BigInt.zero, + {}, TrivialNumber(0), {}, DateTime.fromMillisecondsSinceEpoch(0), + TrivialNumber(0), + {}, ); k.JsonConverterTestClass jsonConverterFromJson(Map json) => @@ -203,9 +207,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { this.durationList, this.bigInt, this.bigIntMap, + this.nullableBigInt, + this.nullableBigIntMap, this.numberSilly, this.numberSillySet, this.dateTime, + this.nullableNumberSilly, + this.nullableNumberSillySet, ); factory JsonConverterTestClass.fromJson(Map json) => @@ -219,10 +227,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { BigInt bigInt; Map bigIntMap; + BigInt? nullableBigInt; + Map nullableBigIntMap; + TrivialNumber numberSilly; Set numberSillySet; DateTime? dateTime; + + TrivialNumber? nullableNumberSilly; + Set nullableNumberSillySet; } @JsonSerializable() diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g.dart index ec7fa097c..c74f6aee0 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g.dart @@ -146,11 +146,24 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson( (k, e) => MapEntry(k, const BigIntStringConverter().fromJson(e as String)), ), + _$JsonConverterFromJson( + json['nullableBigInt'], const BigIntStringConverter().fromJson), + (json['nullableBigIntMap'] as Map).map( + (k, e) => MapEntry( + k, + _$JsonConverterFromJson( + e, const BigIntStringConverter().fromJson)), + ), TrivialNumberConverter.instance.fromJson(json['numberSilly'] as int?), (json['numberSillySet'] as List) .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) .toSet(), const EpochDateTimeConverter().fromJson(json['dateTime'] as int?), + TrivialNumberConverter.instance + .fromJson(json['nullableNumberSilly'] as int?), + (json['nullableNumberSillySet'] as List) + .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) + .toSet(), ); Map _$JsonConverterTestClassToJson( @@ -162,14 +175,38 @@ Map _$JsonConverterTestClassToJson( 'bigInt': const BigIntStringConverter().toJson(instance.bigInt), 'bigIntMap': instance.bigIntMap .map((k, e) => MapEntry(k, const BigIntStringConverter().toJson(e))), + 'nullableBigInt': _$JsonConverterToJson( + instance.nullableBigInt, const BigIntStringConverter().toJson), + 'nullableBigIntMap': instance.nullableBigIntMap.map((k, e) => MapEntry( + k, + _$JsonConverterToJson( + e, const BigIntStringConverter().toJson))), 'numberSilly': TrivialNumberConverter.instance.toJson(instance.numberSilly), 'numberSillySet': instance.numberSillySet .map(TrivialNumberConverter.instance.toJson) .toList(), 'dateTime': const EpochDateTimeConverter().toJson(instance.dateTime), + 'nullableNumberSilly': _$JsonConverterToJson( + instance.nullableNumberSilly, TrivialNumberConverter.instance.toJson), + 'nullableNumberSillySet': instance.nullableNumberSillySet + .map((e) => _$JsonConverterToJson( + e, TrivialNumberConverter.instance.toJson)) + .toList(), }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + JsonConverterGeneric _$JsonConverterGenericFromJson( Map json) => JsonConverterGeneric( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart index bf82df50d..9da91757a 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart @@ -65,9 +65,13 @@ class _Factory implements k.KitchenSinkFactory { [], BigInt.zero, {}, + BigInt.zero, + {}, TrivialNumber(0), {}, DateTime.fromMillisecondsSinceEpoch(0), + TrivialNumber(0), + {}, ); k.JsonConverterTestClass jsonConverterFromJson(Map json) => @@ -205,9 +209,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { this.durationList, this.bigInt, this.bigIntMap, + this.nullableBigInt, + this.nullableBigIntMap, this.numberSilly, this.numberSillySet, this.dateTime, + this.nullableNumberSilly, + this.nullableNumberSillySet, ); factory JsonConverterTestClass.fromJson(Map json) => @@ -221,10 +229,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { BigInt bigInt; Map bigIntMap; + BigInt? nullableBigInt; + Map nullableBigIntMap; + TrivialNumber numberSilly; Set numberSillySet; DateTime? dateTime; + + TrivialNumber? nullableNumberSilly; + Set nullableNumberSillySet; } @JsonSerializable( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.g.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.g.dart index 16d995c98..638091dcc 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.g.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.g.dart @@ -137,11 +137,24 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson(Map json) => (k, e) => MapEntry( k as String, const BigIntStringConverter().fromJson(e as String)), ), + _$JsonConverterFromJson( + json['nullableBigInt'], const BigIntStringConverter().fromJson), + (json['nullableBigIntMap'] as Map).map( + (k, e) => MapEntry( + k as String, + _$JsonConverterFromJson( + e, const BigIntStringConverter().fromJson)), + ), TrivialNumberConverter.instance.fromJson(json['numberSilly'] as int?), (json['numberSillySet'] as List) .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) .toSet(), const EpochDateTimeConverter().fromJson(json['dateTime'] as int?), + TrivialNumberConverter.instance + .fromJson(json['nullableNumberSilly'] as int?), + (json['nullableNumberSillySet'] as List) + .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) + .toSet(), ); Map _$JsonConverterTestClassToJson( @@ -153,14 +166,38 @@ Map _$JsonConverterTestClassToJson( 'bigInt': const BigIntStringConverter().toJson(instance.bigInt), 'bigIntMap': instance.bigIntMap .map((k, e) => MapEntry(k, const BigIntStringConverter().toJson(e))), + 'nullableBigInt': _$JsonConverterToJson( + instance.nullableBigInt, const BigIntStringConverter().toJson), + 'nullableBigIntMap': instance.nullableBigIntMap.map((k, e) => MapEntry( + k, + _$JsonConverterToJson( + e, const BigIntStringConverter().toJson))), 'numberSilly': TrivialNumberConverter.instance.toJson(instance.numberSilly), 'numberSillySet': instance.numberSillySet .map(TrivialNumberConverter.instance.toJson) .toList(), 'dateTime': const EpochDateTimeConverter().toJson(instance.dateTime), + 'nullableNumberSilly': _$JsonConverterToJson( + instance.nullableNumberSilly, TrivialNumberConverter.instance.toJson), + 'nullableNumberSillySet': instance.nullableNumberSillySet + .map((e) => _$JsonConverterToJson( + e, TrivialNumberConverter.instance.toJson)) + .toList(), }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + JsonConverterGeneric _$JsonConverterGenericFromJson( Map json) => JsonConverterGeneric( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.dart index 0a5119aa5..71812ba2e 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.dart @@ -65,9 +65,13 @@ class _Factory implements k.KitchenSinkFactory { [], BigInt.zero, {}, + BigInt.zero, + {}, TrivialNumber(0), {}, DateTime.fromMillisecondsSinceEpoch(0), + TrivialNumber(0), + {}, ); k.JsonConverterTestClass jsonConverterFromJson(Map json) => @@ -207,9 +211,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { this.durationList, this.bigInt, this.bigIntMap, + this.nullableBigInt, + this.nullableBigIntMap, this.numberSilly, this.numberSillySet, this.dateTime, + this.nullableNumberSilly, + this.nullableNumberSillySet, ); factory JsonConverterTestClass.fromJson(Map json) => @@ -223,10 +231,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { BigInt bigInt; Map bigIntMap; + BigInt? nullableBigInt; + Map nullableBigIntMap; + TrivialNumber numberSilly; Set numberSillySet; DateTime? dateTime; + + TrivialNumber? nullableNumberSilly; + Set nullableNumberSillySet; } @JsonSerializable( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.g.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.g.dart index 8be98d6cb..1d8c4c26b 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.g.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_any_map__checked.g.dart @@ -198,6 +198,18 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson(Map json) => (k, e) => MapEntry(k as String, const BigIntStringConverter().fromJson(e as String)), )), + $checkedConvert( + 'nullableBigInt', + (v) => _$JsonConverterFromJson( + v, const BigIntStringConverter().fromJson)), + $checkedConvert( + 'nullableBigIntMap', + (v) => (v as Map).map( + (k, e) => MapEntry( + k as String, + _$JsonConverterFromJson( + e, const BigIntStringConverter().fromJson)), + )), $checkedConvert('numberSilly', (v) => TrivialNumberConverter.instance.fromJson(v as int?)), $checkedConvert( @@ -208,6 +220,14 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson(Map json) => .toSet()), $checkedConvert('dateTime', (v) => const EpochDateTimeConverter().fromJson(v as int?)), + $checkedConvert('nullableNumberSilly', + (v) => TrivialNumberConverter.instance.fromJson(v as int?)), + $checkedConvert( + 'nullableNumberSillySet', + (v) => (v as List) + .map((e) => + TrivialNumberConverter.instance.fromJson(e as int?)) + .toSet()), ); return val; }, @@ -222,14 +242,38 @@ Map _$JsonConverterTestClassToJson( 'bigInt': const BigIntStringConverter().toJson(instance.bigInt), 'bigIntMap': instance.bigIntMap .map((k, e) => MapEntry(k, const BigIntStringConverter().toJson(e))), + 'nullableBigInt': _$JsonConverterToJson( + instance.nullableBigInt, const BigIntStringConverter().toJson), + 'nullableBigIntMap': instance.nullableBigIntMap.map((k, e) => MapEntry( + k, + _$JsonConverterToJson( + e, const BigIntStringConverter().toJson))), 'numberSilly': TrivialNumberConverter.instance.toJson(instance.numberSilly), 'numberSillySet': instance.numberSillySet .map(TrivialNumberConverter.instance.toJson) .toList(), 'dateTime': const EpochDateTimeConverter().toJson(instance.dateTime), + 'nullableNumberSilly': _$JsonConverterToJson( + instance.nullableNumberSilly, TrivialNumberConverter.instance.toJson), + 'nullableNumberSillySet': instance.nullableNumberSillySet + .map((e) => _$JsonConverterToJson( + e, TrivialNumberConverter.instance.toJson)) + .toList(), }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + JsonConverterGeneric _$JsonConverterGenericFromJson( Map json) => $checkedCreate( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.dart index 5927b1aaa..a8fd5e1d8 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.dart @@ -66,9 +66,13 @@ class _Factory implements k.KitchenSinkFactory { [], BigInt.zero, {}, + BigInt.zero, + {}, TrivialNumber(0), {}, DateTime.fromMillisecondsSinceEpoch(0), + TrivialNumber(0), + {}, ); k.JsonConverterTestClass jsonConverterFromJson(Map json) => @@ -207,9 +211,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { this.durationList, this.bigInt, this.bigIntMap, + this.nullableBigInt, + this.nullableBigIntMap, this.numberSilly, this.numberSillySet, this.dateTime, + this.nullableNumberSilly, + this.nullableNumberSillySet, ); factory JsonConverterTestClass.fromJson(Map json) => @@ -223,10 +231,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { BigInt bigInt; Map bigIntMap; + BigInt? nullableBigInt; + Map nullableBigIntMap; + TrivialNumber numberSilly; Set numberSillySet; DateTime? dateTime; + + TrivialNumber? nullableNumberSilly; + Set nullableNumberSillySet; } @JsonSerializable( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.g.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.g.dart index 6afeb29a2..a0246e8bc 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.g.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_exclude_null.g.dart @@ -154,11 +154,24 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson( (k, e) => MapEntry(k, const BigIntStringConverter().fromJson(e as String)), ), + _$JsonConverterFromJson( + json['nullableBigInt'], const BigIntStringConverter().fromJson), + (json['nullableBigIntMap'] as Map).map( + (k, e) => MapEntry( + k, + _$JsonConverterFromJson( + e, const BigIntStringConverter().fromJson)), + ), TrivialNumberConverter.instance.fromJson(json['numberSilly'] as int?), (json['numberSillySet'] as List) .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) .toSet(), const EpochDateTimeConverter().fromJson(json['dateTime'] as int?), + TrivialNumberConverter.instance + .fromJson(json['nullableNumberSilly'] as int?), + (json['nullableNumberSillySet'] as List) + .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) + .toSet(), ); Map _$JsonConverterTestClassToJson( @@ -177,6 +190,14 @@ Map _$JsonConverterTestClassToJson( writeNotNull('bigInt', const BigIntStringConverter().toJson(instance.bigInt)); val['bigIntMap'] = instance.bigIntMap .map((k, e) => MapEntry(k, const BigIntStringConverter().toJson(e))); + writeNotNull( + 'nullableBigInt', + _$JsonConverterToJson( + instance.nullableBigInt, const BigIntStringConverter().toJson)); + val['nullableBigIntMap'] = instance.nullableBigIntMap.map((k, e) => MapEntry( + k, + _$JsonConverterToJson( + e, const BigIntStringConverter().toJson))); writeNotNull('numberSilly', TrivialNumberConverter.instance.toJson(instance.numberSilly)); val['numberSillySet'] = instance.numberSillySet @@ -184,9 +205,29 @@ Map _$JsonConverterTestClassToJson( .toList(); writeNotNull( 'dateTime', const EpochDateTimeConverter().toJson(instance.dateTime)); + writeNotNull( + 'nullableNumberSilly', + _$JsonConverterToJson(instance.nullableNumberSilly, + TrivialNumberConverter.instance.toJson)); + val['nullableNumberSillySet'] = instance.nullableNumberSillySet + .map((e) => _$JsonConverterToJson( + e, TrivialNumberConverter.instance.toJson)) + .toList(); return val; } +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + JsonConverterGeneric _$JsonConverterGenericFromJson( Map json) => JsonConverterGeneric( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.dart index bce55d5f0..f5f13fc7d 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.dart @@ -66,9 +66,13 @@ class _Factory implements k.KitchenSinkFactory { [], BigInt.zero, {}, + BigInt.zero, + {}, TrivialNumber(0), {}, DateTime.fromMillisecondsSinceEpoch(0), + TrivialNumber(0), + {}, ); k.JsonConverterTestClass jsonConverterFromJson(Map json) => @@ -207,9 +211,13 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { this.durationList, this.bigInt, this.bigIntMap, + this.nullableBigInt, + this.nullableBigIntMap, this.numberSilly, this.numberSillySet, this.dateTime, + this.nullableNumberSilly, + this.nullableNumberSillySet, ); factory JsonConverterTestClass.fromJson(Map json) => @@ -223,10 +231,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass { BigInt bigInt; Map bigIntMap; + BigInt? nullableBigInt; + Map nullableBigIntMap; + TrivialNumber numberSilly; Set numberSillySet; DateTime? dateTime; + + TrivialNumber? nullableNumberSilly; + Set nullableNumberSillySet; } @JsonSerializable( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.g.dart b/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.g.dart index cb0415623..e6f218fa6 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.g.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink.g_explicit_to_json.g.dart @@ -148,11 +148,24 @@ JsonConverterTestClass _$JsonConverterTestClassFromJson( (k, e) => MapEntry(k, const BigIntStringConverter().fromJson(e as String)), ), + _$JsonConverterFromJson( + json['nullableBigInt'], const BigIntStringConverter().fromJson), + (json['nullableBigIntMap'] as Map).map( + (k, e) => MapEntry( + k, + _$JsonConverterFromJson( + e, const BigIntStringConverter().fromJson)), + ), TrivialNumberConverter.instance.fromJson(json['numberSilly'] as int?), (json['numberSillySet'] as List) .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) .toSet(), const EpochDateTimeConverter().fromJson(json['dateTime'] as int?), + TrivialNumberConverter.instance + .fromJson(json['nullableNumberSilly'] as int?), + (json['nullableNumberSillySet'] as List) + .map((e) => TrivialNumberConverter.instance.fromJson(e as int?)) + .toSet(), ); Map _$JsonConverterTestClassToJson( @@ -164,14 +177,38 @@ Map _$JsonConverterTestClassToJson( 'bigInt': const BigIntStringConverter().toJson(instance.bigInt), 'bigIntMap': instance.bigIntMap .map((k, e) => MapEntry(k, const BigIntStringConverter().toJson(e))), + 'nullableBigInt': _$JsonConverterToJson( + instance.nullableBigInt, const BigIntStringConverter().toJson), + 'nullableBigIntMap': instance.nullableBigIntMap.map((k, e) => MapEntry( + k, + _$JsonConverterToJson( + e, const BigIntStringConverter().toJson))), 'numberSilly': TrivialNumberConverter.instance.toJson(instance.numberSilly), 'numberSillySet': instance.numberSillySet .map(TrivialNumberConverter.instance.toJson) .toList(), 'dateTime': const EpochDateTimeConverter().toJson(instance.dateTime), + 'nullableNumberSilly': _$JsonConverterToJson( + instance.nullableNumberSilly, TrivialNumberConverter.instance.toJson), + 'nullableNumberSillySet': instance.nullableNumberSillySet + .map((e) => _$JsonConverterToJson( + e, TrivialNumberConverter.instance.toJson)) + .toList(), }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + JsonConverterGeneric _$JsonConverterGenericFromJson( Map json) => JsonConverterGeneric( diff --git a/json_serializable/test/kitchen_sink/kitchen_sink_interface.dart b/json_serializable/test/kitchen_sink/kitchen_sink_interface.dart index f69ade258..9f8740943 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink_interface.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink_interface.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import '../test_utils.dart'; +import 'json_converters.dart'; import 'simple_object.dart'; /// A key name that requires special encoding @@ -41,6 +42,8 @@ abstract class KitchenSinkFactory { } abstract class JsonConverterTestClass { + TrivialNumber? nullableNumberSilly; + Map toJson(); } diff --git a/json_serializable/test/kitchen_sink/kitchen_sink_test.dart b/json_serializable/test/kitchen_sink/kitchen_sink_test.dart index 8044c29d7..99e45bf40 100644 --- a/json_serializable/test/kitchen_sink/kitchen_sink_test.dart +++ b/json_serializable/test/kitchen_sink/kitchen_sink_test.dart @@ -6,6 +6,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:test/test.dart'; import '../test_utils.dart'; +import 'json_converters.dart'; import 'kitchen_sink.factories.dart'; import 'kitchen_sink_interface.dart'; import 'kitchen_sink_test_shared.dart'; @@ -54,10 +55,14 @@ const _jsonConverterValidValues = { 'duration': 5, 'durationList': [5], 'bigInt': '5', - 'bigIntMap': {'vaule': '5'}, + 'bigIntMap': {'value': '5'}, 'numberSilly': 5, 'numberSillySet': [5], - 'dateTime': 5 + 'dateTime': 5, + 'nullableNumberSilly': 5, + 'nullableBigInt': '42', + 'nullableBigIntMap': {'value': '42'}, + 'nullableNumberSillySet': [42], }; void _nonNullableTests(KitchenSinkFactory factory) { @@ -96,9 +101,13 @@ void _nullableTests(KitchenSinkFactory factory) { 'durationList': [], 'bigInt': '0', 'bigIntMap': {}, + 'nullableBigInt': '0', + 'nullableBigIntMap': {}, 'numberSilly': 0, 'numberSillySet': [], - 'dateTime': 0 + 'dateTime': 0, + 'nullableNumberSilly': 0, + 'nullableNumberSillySet': [], }); expect(json.keys, unorderedEquals(_jsonConverterValidValues.keys)); @@ -183,6 +192,18 @@ void _sharedTests(KitchenSinkFactory factory) { roundTripObject(item, factory.fromJson); }); + test('JsonConverters with nullable JSON keys handle `null` JSON values', () { + final item = factory.jsonConverterFromJson({ + ..._jsonConverterValidValues, + 'nullableNumberSilly': null, + }); + + expect( + item.nullableNumberSilly, + isA().having((e) => e.value, 'value', isNull), + ); + }); + test('list and map of DateTime - not null', () { final now = DateTime.now(); final item = factory.ctor(dateTimeIterable: [now]) diff --git a/json_serializable/test/src/json_converter_test_input.dart b/json_serializable/test/src/json_converter_test_input.dart index 10d029b41..fbe7da28c 100644 --- a/json_serializable/test/src/json_converter_test_input.dart +++ b/json_serializable/test/src/json_converter_test_input.dart @@ -189,3 +189,25 @@ class _NeedsConversionConverter @override int toJson(_NeedsConversion object) => 0; } + +@ShouldThrow( + ''' +Could not generate `fromJson` code for `value`. +To support the type `_NeedsConversion` you can: +$converterOrKeyInstructions''', +) +@_NullableConverter() +@JsonSerializable() +class JsonConverterNullableToNonNullable { + late _NeedsConversion value; +} + +class _NullableConverter implements JsonConverter<_NeedsConversion?, Object?> { + const _NullableConverter(); + + @override + _NeedsConversion? fromJson(Object? json) => null; + + @override + Object? toJson(_NeedsConversion? object) => null; +}