Skip to content

Commit 6645c79

Browse files
committed
Fix detection of toJson/fromJson in nested types
1 parent f8dd6b8 commit 6645c79

File tree

6 files changed

+112
-42
lines changed

6 files changed

+112
-42
lines changed

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
* Small change to how nullable `Map` values are deserialized.
66

7+
* Improve detection of `toJson` and `fromJson` in nested types.
8+
79
## 0.5.4+1
810

911
* Fixed a bug introduced in `0.5.4` in some cases where enum values are nested

json_serializable/lib/src/generator_helper.dart

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Future<String> generate(JsonSerializableGenerator generator, Element element,
2727
}
2828

2929
var classElement = element as ClassElement;
30-
var classAnnotation = _valueForAnnotation(annotation);
30+
var classAnnotation = valueForAnnotation(annotation);
3131
var helper = new _GeneratorHelper(generator, classElement, classAnnotation);
3232
return helper._generate();
3333
}
@@ -441,13 +441,6 @@ String _nameAccess(FieldElement field) => jsonKeyFor(field).name ?? field.name;
441441
String _safeNameAccess(FieldElement field) =>
442442
escapeDartString(_nameAccess(field));
443443

444-
JsonSerializable _valueForAnnotation(ConstantReader annotation) =>
445-
new JsonSerializable(
446-
createToJson: annotation.read('createToJson').boolValue,
447-
createFactory: annotation.read('createFactory').boolValue,
448-
nullable: annotation.read('nullable').boolValue,
449-
includeIfNull: annotation.read('includeIfNull').boolValue);
450-
451444
InvalidGenerationSourceError _createInvalidGenerationError(
452445
String targetMember, FieldElement field, UnsupportedTypeError e) {
453446
var extra = (field.type != e.type) ? ' because of type `${e.type}`' : '';

json_serializable/lib/src/type_helpers/json_helper.dart

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
import 'package:analyzer/dart/element/element.dart';
66
import 'package:analyzer/dart/element/type.dart';
7+
8+
import 'package:json_annotation/json_annotation.dart';
9+
import 'package:source_gen/source_gen.dart';
10+
711
import '../shared_checkers.dart';
812
import '../type_helper.dart';
13+
import '../type_helper_context.dart';
914
import '../utils.dart';
1015

1116
class JsonHelper extends TypeHelper {
@@ -17,11 +22,7 @@ class JsonHelper extends TypeHelper {
1722
/// provided objects.
1823
@override
1924
String serialize(DartType targetType, String expression, _) {
20-
// TODO(kevmoo): This should be checking for toJson method, but toJson might
21-
// be gone during generation, so we'll have to check for the annotation.
22-
// In the mean time, just assume the `canSerialize` logic will work most of
23-
// the time.
24-
if (!_canDeserialize(targetType)) {
25+
if (!_canSerialize(targetType)) {
2526
return null;
2627
}
2728

@@ -31,17 +32,30 @@ class JsonHelper extends TypeHelper {
3132
@override
3233
String deserialize(
3334
DartType targetType, String expression, DeserializeContext context) {
34-
if (!_canDeserialize(targetType)) {
35+
if (targetType is! InterfaceType) {
3536
return null;
3637
}
3738

38-
var classElement = targetType.element as ClassElement;
39-
var fromJsonCtor =
40-
classElement.constructors.firstWhere((ce) => ce.name == 'fromJson');
41-
// TODO: should verify that this type is a valid JSON type...but for now...
42-
var asCastType = fromJsonCtor.parameters.first.type;
39+
var type = targetType as InterfaceType;
40+
var classElement = type.element;
41+
42+
var fromJsonCtor = classElement.constructors
43+
.singleWhere((ce) => ce.name == 'fromJson', orElse: () => null);
4344

44-
var asCast = asStatement(asCastType);
45+
String asCast;
46+
if (fromJsonCtor != null) {
47+
// TODO: should verify that this type is a valid JSON type...but for now...
48+
var asCastType = fromJsonCtor.parameters.first.type;
49+
asCast = asStatement(asCastType);
50+
} else if (_getAnnotation(type)?.createFactory == true) {
51+
if ((context as TypeHelperContext).anyMap) {
52+
asCast = ' as Map';
53+
} else {
54+
asCast = ' as Map<String, dynamic>';
55+
}
56+
} else {
57+
return null;
58+
}
4559

4660
// TODO: the type could be imported from a library with a prefix!
4761
// github.com/dart-lang/json_serializable/issues/19
@@ -51,17 +65,55 @@ class JsonHelper extends TypeHelper {
5165
}
5266
}
5367

54-
bool _canDeserialize(DartType type) {
55-
if (type is! InterfaceType) return false;
68+
bool _canSerialize(DartType type) {
69+
if (type is InterfaceType) {
70+
var toJsonMethod = _getMethod(type, 'toJson');
5671

57-
var classElement = type.element as ClassElement;
72+
if (toJsonMethod != null) {
73+
// TODO: validate there are no required parameters
74+
return true;
75+
}
5876

59-
for (var ctor in classElement.constructors) {
60-
if (ctor.name == 'fromJson') {
61-
// TODO: validate that there are the right number and type of arguments
77+
if (_getAnnotation(type)?.createToJson == true) {
78+
// TODO: consider logging that we're assuming a user will wire up the
79+
// generated mixin at some point...
6280
return true;
6381
}
6482
}
65-
6683
return false;
6784
}
85+
86+
JsonSerializable _getAnnotation(InterfaceType source) {
87+
var annotations = const TypeChecker.fromRuntime(JsonSerializable)
88+
.annotationsOfExact(source.element, throwOnUnresolved: false)
89+
.toList();
90+
91+
if (annotations.isEmpty) {
92+
return null;
93+
}
94+
95+
return valueForAnnotation(new ConstantReader(annotations.single));
96+
}
97+
98+
MethodElement _getMethod(DartType type, String methodName) {
99+
if (type is InterfaceType) {
100+
var method = type.element.getMethod(methodName);
101+
if (method != null) {
102+
return method;
103+
}
104+
105+
var match = [type.interfaces, type.mixins]
106+
.expand((e) => e)
107+
.map((type) => _getMethod(type, methodName))
108+
.firstWhere((value) => value != null, orElse: () => null);
109+
110+
if (match != null) {
111+
return match;
112+
}
113+
114+
if (type.superclass != null) {
115+
return _getMethod(type.superclass, methodName);
116+
}
117+
}
118+
return null;
119+
}

json_serializable/lib/src/utils.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ import 'package:analyzer/dart/element/type.dart';
1010
import 'package:analyzer/src/dart/resolver/inheritance_manager.dart'
1111
show InheritanceManager;
1212

13+
import 'package:json_annotation/json_annotation.dart';
1314
import 'package:source_gen/source_gen.dart';
1415

16+
JsonSerializable valueForAnnotation(ConstantReader annotation) =>
17+
new JsonSerializable(
18+
createToJson: annotation.read('createToJson').boolValue,
19+
createFactory: annotation.read('createFactory').boolValue,
20+
nullable: annotation.read('nullable').boolValue,
21+
includeIfNull: annotation.read('includeIfNull').boolValue);
22+
1523
bool isEnum(DartType targetType) =>
1624
targetType is InterfaceType && targetType.element.isEnum;
1725

json_serializable/test/json_serializable_test.dart

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,22 @@ void main() {
5151
() => _registerTests(const JsonSerializableGenerator(useWrappers: true)));
5252
}
5353

54+
Future<String> _runForElementNamed(
55+
JsonSerializableGenerator generator, String name) async {
56+
var library = new LibraryReader(_compilationUnit.element.library);
57+
var element = library.allElements.singleWhere((e) => e.name == name);
58+
var annotation = generator.typeChecker.firstAnnotationOf(element);
59+
var generated = await generator.generateForAnnotatedElement(
60+
element, new ConstantReader(annotation), null);
61+
62+
var output = _formatter.format(generated);
63+
printOnFailure(output);
64+
return output;
65+
}
66+
5467
void _registerTests(JsonSerializableGenerator generator) {
55-
Future<String> runForElementNamed(String name) async {
56-
var library = new LibraryReader(_compilationUnit.element.library);
57-
var element = library.allElements.singleWhere((e) => e.name == name);
58-
var annotation = generator.typeChecker.firstAnnotationOf(element);
59-
var generated = await generator.generateForAnnotatedElement(
60-
element, new ConstantReader(annotation), null);
61-
62-
var output = _formatter.format(generated);
63-
printOnFailure(output);
64-
return output;
65-
}
68+
Future<String> runForElementNamed(String name) =>
69+
_runForElementNamed(generator, name);
6670

6771
void expectThrows(String elementName, messageMatcher, [todoMatcher]) {
6872
todoMatcher ??= isEmpty;
@@ -228,7 +232,20 @@ abstract class _$OrderSerializerMixin {
228232
test('class with child json-able object', () async {
229233
var output = await runForElementNamed('ParentObject');
230234

231-
expect(output, contains('new ChildObject.fromJson'));
235+
expect(
236+
output,
237+
contains("new ChildObject.fromJson(json['child'] "
238+
'as Map<String, dynamic>)'));
239+
});
240+
241+
test('class with child json-able object - anyMap', () async {
242+
var output = await _runForElementNamed(
243+
new JsonSerializableGenerator(
244+
anyMap: true, useWrappers: generator.useWrappers),
245+
'ParentObject');
246+
247+
expect(
248+
output, contains("new ChildObject.fromJson(json['child'] as Map)"));
232249
});
233250

234251
test('class with child list of json-able objects', () async {

json_serializable/test/src/json_serializable_test_input.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class FinalFieldsNotSetInCtor {
5252
FinalFieldsNotSetInCtor();
5353
}
5454

55-
@JsonSerializable()
55+
@JsonSerializable(createToJson: false)
5656
class FromJsonOptionalParameters {
5757
final ChildWithFromJson child;
5858

@@ -74,8 +74,6 @@ class ParentObject {
7474
class ChildObject {
7575
int number;
7676
String str;
77-
78-
factory ChildObject.fromJson(json) => null;
7977
}
8078

8179
@JsonSerializable()

0 commit comments

Comments
 (0)