Skip to content

Commit

Permalink
Support using non-nullable JsonConverter on nullable properties (#1136)
Browse files Browse the repository at this point in the history
fixes #822
  • Loading branch information
rrousselGit committed Apr 28, 2022
1 parent 5dbb1b2 commit 09cca32
Show file tree
Hide file tree
Showing 18 changed files with 426 additions and 16 deletions.
5 changes: 5 additions & 0 deletions json_serializable/CHANGELOG.md
@@ -1,3 +1,8 @@
## 6.3.0-dev

- Added support for using a `JsonConverter<MyClass, Object>` 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
Expand Down
90 changes: 84 additions & 6 deletions json_serializable/lib/src/type_helpers/json_converter_helper.dart
Expand Up @@ -32,6 +32,24 @@ class JsonConverterHelper extends TypeHelper {
return null;
}

if (!converter.fieldType.isNullableType && targetType.isNullableType) {
const converterToJsonName = r'_$JsonConverterToJson';
context.addMember('''
Json? $converterToJsonName<Json, Value>(
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');
}

Expand All @@ -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<Json, Value>(
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',
Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -145,18 +212,21 @@ _JsonConvertData? _typeConverterFrom(
match.genericTypeArg!,
reviver.accessor,
match.jsonType,
match.fieldType,
);
}

return _JsonConvertData.className(
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;
Expand All @@ -166,6 +236,7 @@ class _ConverterMatch {
this.annotation,
this.jsonType,
this.genericTypeArg,
this.fieldType,
);
}

Expand All @@ -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) {
Expand All @@ -212,6 +289,7 @@ _ConverterMatch? _compatibleMatch(
constantValue,
jsonConverterSuper.typeArguments[1],
'${targetType.element.name}${targetType.isNullableType ? '?' : ''}',
fieldType,
);
}

Expand Down
2 changes: 1 addition & 1 deletion 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.
Expand Down
26 changes: 20 additions & 6 deletions json_serializable/test/generic_files/generic_class.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions json_serializable/test/json_serializable_test.dart
Expand Up @@ -85,6 +85,7 @@ const _expectedAnnotatedTests = {
'JsonConverterCtorParams',
'JsonConverterDuplicateAnnotations',
'JsonConverterNamedCtor',
'JsonConverterNullableToNonNullable',
'JsonConverterOnGetter',
'JsonConverterWithBadTypeArg',
'JsonValueValid',
Expand Down
14 changes: 14 additions & 0 deletions json_serializable/test/kitchen_sink/kitchen_sink.dart
Expand Up @@ -66,9 +66,13 @@ class _Factory implements k.KitchenSinkFactory<String, dynamic> {
[],
BigInt.zero,
{},
BigInt.zero,
{},
TrivialNumber(0),
{},
DateTime.fromMillisecondsSinceEpoch(0),
TrivialNumber(0),
{},
);

k.JsonConverterTestClass jsonConverterFromJson(Map<String, dynamic> json) =>
Expand Down Expand Up @@ -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<String, dynamic> json) =>
Expand All @@ -219,10 +227,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
BigInt bigInt;
Map<String, BigInt> bigIntMap;

BigInt? nullableBigInt;
Map<String, BigInt?> nullableBigIntMap;

TrivialNumber numberSilly;
Set<TrivialNumber> numberSillySet;

DateTime? dateTime;

TrivialNumber? nullableNumberSilly;
Set<TrivialNumber?> nullableNumberSillySet;
}

@JsonSerializable()
Expand Down
37 changes: 37 additions & 0 deletions json_serializable/test/kitchen_sink/kitchen_sink.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions json_serializable/test/kitchen_sink/kitchen_sink.g_any_map.dart
Expand Up @@ -65,9 +65,13 @@ class _Factory implements k.KitchenSinkFactory<dynamic, dynamic> {
[],
BigInt.zero,
{},
BigInt.zero,
{},
TrivialNumber(0),
{},
DateTime.fromMillisecondsSinceEpoch(0),
TrivialNumber(0),
{},
);

k.JsonConverterTestClass jsonConverterFromJson(Map<String, dynamic> json) =>
Expand Down Expand Up @@ -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<String, dynamic> json) =>
Expand All @@ -221,10 +229,16 @@ class JsonConverterTestClass implements k.JsonConverterTestClass {
BigInt bigInt;
Map<String, BigInt> bigIntMap;

BigInt? nullableBigInt;
Map<String, BigInt?> nullableBigIntMap;

TrivialNumber numberSilly;
Set<TrivialNumber> numberSillySet;

DateTime? dateTime;

TrivialNumber? nullableNumberSilly;
Set<TrivialNumber?> nullableNumberSillySet;
}

@JsonSerializable(
Expand Down

0 comments on commit 09cca32

Please sign in to comment.