Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using non-nullable JsonConverter on nullable properties #1136

Merged
merged 7 commits into from Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';
kevmoo marked this conversation as resolved.
Show resolved Hide resolved
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