Skip to content

Commit 0184ef8

Browse files
committed
Add support for generic classes
Fixes #116
1 parent b75b439 commit 0184ef8

16 files changed

+445
-33
lines changed

json_serializable/CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
* Add `checked` configuration option. If `true`, generated `fromJson` functions
44
include extra checks to validate proper deserialization of types.
55

6-
* Use `Map.map` for more map conversions. Simplifies generated code and fixes
7-
a subtle issue when the `Map` key type is `dynamic` or `Object`.
8-
96
* Added `any_map` to configuration. Allows `fromJson` code to
107
support dynamic `Map` instances that are not explicitly
118
`Map<String, dynaimc>`.
129

10+
* Added support for classes with type arguments.
11+
12+
* Use `Map.map` for more map conversions. Simplifies generated code and fixes
13+
a subtle issue when the `Map` key type is `dynamic` or `Object`.
14+
1315
## 0.5.3
1416

1517
* Require the latest version of `package:analyzer` - `v0.32.0`.

json_serializable/lib/src/generator_helper.dart

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,20 @@ class _GeneratorHelper {
3939
final StringBuffer _buffer = new StringBuffer();
4040

4141
String get _prefix => '_\$${_element.name}';
42-
String get _mixClassName => '${_prefix}SerializerMixin';
43-
String get _helpClassName => '${_prefix}JsonMapWrapper';
42+
String _mixinClassName(bool withConstraints) =>
43+
'${_prefix}SerializerMixin${_genericClassArguments(withConstraints)}';
44+
String _wrapperClassName([bool withConstraints]) =>
45+
'${_prefix}JsonMapWrapper${_genericClassArguments(withConstraints)}';
46+
47+
String _genericClassArguments(bool withConstraints) {
48+
if (withConstraints == null) {
49+
return '';
50+
}
51+
return genericClassArguments(_element, withConstraints);
52+
}
53+
54+
String get _targetClassReference =>
55+
'${_element.name}${_genericClassArguments(false)}';
4456

4557
_GeneratorHelper(this._generator, this._element, this._annotation);
4658

@@ -66,7 +78,7 @@ class _GeneratorHelper {
6678
//
6779
// Generate the mixin class
6880
//
69-
_buffer.writeln('abstract class $_mixClassName {');
81+
_buffer.writeln('abstract class ${_mixinClassName(true)} {');
7082

7183
// write copies of the fields - this allows the toJson method to access
7284
// the fields of the target class
@@ -80,7 +92,7 @@ class _GeneratorHelper {
8092
var writeNaive = accessibleFields.every(_writeJsonValueNaive);
8193

8294
if (_generator.useWrappers) {
83-
_buffer.writeln('=> new $_helpClassName(this);');
95+
_buffer.writeln('=> new ${_wrapperClassName(false)}(this);');
8496
} else {
8597
if (writeNaive) {
8698
// write simple `toJson` method that includes all keys...
@@ -126,7 +138,8 @@ class _GeneratorHelper {
126138
if (_annotation.createFactory) {
127139
_buffer.writeln();
128140
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
129-
_buffer.writeln('${_element.name} ${_prefix}FromJson($mapType json) =>');
141+
_buffer.writeln('$_targetClassReference '
142+
'${_prefix}FromJson${_genericClassArguments(true)}($mapType json) =>');
130143

131144
String deserializeFun(String paramOrFieldName,
132145
{ParameterElement ctorParam}) =>
@@ -221,9 +234,10 @@ class _GeneratorHelper {
221234
void _writeWrapper(Iterable<FieldElement> fields) {
222235
_buffer.writeln();
223236
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
224-
_buffer.writeln('''class $_helpClassName extends \$JsonMapWrapper {
225-
final $_mixClassName _v;
226-
$_helpClassName(this._v);
237+
_buffer
238+
.writeln('''class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
239+
final ${_mixinClassName(false)} _v;
240+
${_wrapperClassName()}(this._v);
227241
''');
228242

229243
if (fields.every(_writeJsonValueNaive)) {

json_serializable/lib/src/json_key_helpers.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,23 @@ ConvertData _getFunctionName(
108108
var argType = functionElement.parameters.first.type;
109109
if (isFrom) {
110110
var returnType = functionElement.returnType;
111-
if (!returnType.isAssignableTo(element.type)) {
111+
112+
if (returnType is TypeParameterType) {
113+
// We keep things simple in this case. We rely on inferred type arguments
114+
// to the `fromJson` function.
115+
// TODO: consider adding error checking here if there is confusion.
116+
} else if (!returnType.isAssignableTo(element.type)) {
112117
_throwUnsupported(
113118
element,
114119
'The `$paramName` function `${functionElement.name}` return type '
115120
'`$returnType` is not compatible with field type `${element.type}`.');
116121
}
117122
} else {
118-
if (!element.type.isAssignableTo(argType)) {
123+
if (argType is TypeParameterType) {
124+
// We keep things simple in this case. We rely on inferred type arguments
125+
// to the `fromJson` function.
126+
// TODO: consider adding error checking here if there is confusion.
127+
} else if (!element.type.isAssignableTo(argType)) {
119128
_throwUnsupported(
120129
element,
121130
'The `$paramName` function `${functionElement.name}` argument type '

json_serializable/lib/src/type_helpers/convert_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class ConvertHelper extends TypeHelper {
1717
DartType targetType, String expression, SerializeContext context) {
1818
var toJsonData = (context as TypeHelperContext).toJsonData;
1919
if (toJsonData != null) {
20-
assert(targetType.isAssignableTo(toJsonData.paramType));
21-
20+
assert(toJsonData.paramType is TypeParameterType ||
21+
targetType.isAssignableTo(toJsonData.paramType));
2222
var result = '${toJsonData.name}($expression)';
2323
return commonNullPrefix(context.nullable, expression, result);
2424
}

json_serializable/lib/src/utils.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,19 @@ int _sortByLocation(FieldElement a, FieldElement b) {
175175

176176
final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);
177177

178-
/// If a parameter is required to invoke the constructor,
178+
String genericClassArguments(ClassElement element, bool withConstraints) {
179+
if (element.typeParameters.isEmpty) {
180+
return '';
181+
}
182+
var values = element.typeParameters.map((t) {
183+
if (withConstraints) {
184+
return t.toString();
185+
}
186+
return t.name;
187+
}).join(',');
188+
return '<$values>';
189+
}
190+
179191
/// [availableConstructorParameters] is checked to see if it is available. If
180192
/// [availableConstructorParameters] does not contain the parameter name,
181193
/// an [UnsupportedError] is thrown.
@@ -243,7 +255,10 @@ CtorData writeConstructorInvocation(
243255
writeableFields.toSet().difference(usedCtorParamsAndFields);
244256

245257
var buffer = new StringBuffer();
246-
buffer.write('new $className(');
258+
//
259+
// Generate the static factory method
260+
//
261+
buffer.write('new $className${genericClassArguments(classElement, false)}(');
247262
buffer.writeAll(
248263
constructorArguments.map((paramElement) =>
249264
deserializeForField(paramElement.name, ctorParam: paramElement)),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// ignore_for_file: annotate_overrides
6+
7+
import 'package:json_annotation/json_annotation.dart';
8+
9+
part 'generic_class.g.dart';
10+
11+
@JsonSerializable()
12+
class GenericClass<T extends num, S> extends Object
13+
with _$GenericClassSerializerMixin<T, S> {
14+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
15+
Object fieldObject;
16+
17+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
18+
dynamic fieldDynamic;
19+
20+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
21+
int fieldInt;
22+
23+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
24+
T fieldT;
25+
26+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
27+
S fieldS;
28+
29+
GenericClass();
30+
31+
factory GenericClass.fromJson(Map<String, dynamic> json) =>
32+
_$GenericClassFromJson<T, S>(json);
33+
}
34+
35+
T _dataFromJson<T, S, U>(Map<String, dynamic> input, [S other1, U, other2]) {
36+
return input['value'] as T;
37+
}
38+
39+
Map<String, dynamic> _dataToJson<T, S, U>(T input, [S other1, U, other2]) {
40+
return {'value': input};
41+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// GENERATED CODE - DO NOT MODIFY BY HAND
6+
7+
part of 'generic_class.dart';
8+
9+
// **************************************************************************
10+
// Generator: JsonSerializableGenerator
11+
// **************************************************************************
12+
13+
GenericClass<T, S> _$GenericClassFromJson<T extends num, S>(
14+
Map<String, dynamic> json) =>
15+
new GenericClass<T, S>()
16+
..fieldObject = json['fieldObject'] == null
17+
? null
18+
: _dataFromJson(json['fieldObject'] as Map<String, dynamic>)
19+
..fieldDynamic = json['fieldDynamic'] == null
20+
? null
21+
: _dataFromJson(json['fieldDynamic'] as Map<String, dynamic>)
22+
..fieldInt = json['fieldInt'] == null
23+
? null
24+
: _dataFromJson(json['fieldInt'] as Map<String, dynamic>)
25+
..fieldT = json['fieldT'] == null
26+
? null
27+
: _dataFromJson(json['fieldT'] as Map<String, dynamic>)
28+
..fieldS = json['fieldS'] == null
29+
? null
30+
: _dataFromJson(json['fieldS'] as Map<String, dynamic>);
31+
32+
abstract class _$GenericClassSerializerMixin<T extends num, S> {
33+
Object get fieldObject;
34+
dynamic get fieldDynamic;
35+
int get fieldInt;
36+
T get fieldT;
37+
S get fieldS;
38+
Map<String, dynamic> toJson() => <String, dynamic>{
39+
'fieldObject': fieldObject == null ? null : _dataToJson(fieldObject),
40+
'fieldDynamic': fieldDynamic == null ? null : _dataToJson(fieldDynamic),
41+
'fieldInt': fieldInt == null ? null : _dataToJson(fieldInt),
42+
'fieldT': fieldT == null ? null : _dataToJson(fieldT),
43+
'fieldS': fieldS == null ? null : _dataToJson(fieldS)
44+
};
45+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// GENERATED CODE - DO NOT MODIFY BY HAND
6+
7+
// **************************************************************************
8+
// Generator: _WrappedGenerator
9+
// **************************************************************************
10+
11+
// ignore_for_file: annotate_overrides
12+
13+
import 'package:json_annotation/json_annotation.dart';
14+
15+
part 'generic_class.wrapped.g.dart';
16+
17+
@JsonSerializable()
18+
class GenericClass<T extends num, S> extends Object
19+
with _$GenericClassSerializerMixin<T, S> {
20+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
21+
Object fieldObject;
22+
23+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
24+
dynamic fieldDynamic;
25+
26+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
27+
int fieldInt;
28+
29+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
30+
T fieldT;
31+
32+
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
33+
S fieldS;
34+
35+
GenericClass();
36+
37+
factory GenericClass.fromJson(Map<String, dynamic> json) =>
38+
_$GenericClassFromJson<T, S>(json);
39+
}
40+
41+
T _dataFromJson<T, S, U>(Map<String, dynamic> input, [S other1, U, other2]) {
42+
print(['fj', T, S, U, input]);
43+
if (T is Set) {
44+
return new Set.from(input['value'] as Iterable) as T;
45+
}
46+
return input['value'] as T;
47+
}
48+
49+
Map<String, dynamic> _dataToJson<T, S, U>(T input, [S other1, U, other2]) {
50+
print(['tj', T, S, U, input]);
51+
dynamic value = input;
52+
if (input is Iterable) {
53+
value = input.toList();
54+
}
55+
return {'value': value};
56+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// GENERATED CODE - DO NOT MODIFY BY HAND
6+
7+
part of 'generic_class.wrapped.dart';
8+
9+
// **************************************************************************
10+
// Generator: JsonSerializableGenerator
11+
// **************************************************************************
12+
13+
GenericClass<T, S> _$GenericClassFromJson<T extends num, S>(
14+
Map<String, dynamic> json) =>
15+
new GenericClass<T, S>()
16+
..fieldObject = json['fieldObject'] == null
17+
? null
18+
: _dataFromJson(json['fieldObject'] as Map<String, dynamic>)
19+
..fieldDynamic = json['fieldDynamic'] == null
20+
? null
21+
: _dataFromJson(json['fieldDynamic'] as Map<String, dynamic>)
22+
..fieldInt = json['fieldInt'] == null
23+
? null
24+
: _dataFromJson(json['fieldInt'] as Map<String, dynamic>)
25+
..fieldT = json['fieldT'] == null
26+
? null
27+
: _dataFromJson(json['fieldT'] as Map<String, dynamic>)
28+
..fieldS = json['fieldS'] == null
29+
? null
30+
: _dataFromJson(json['fieldS'] as Map<String, dynamic>);
31+
32+
abstract class _$GenericClassSerializerMixin<T extends num, S> {
33+
Object get fieldObject;
34+
dynamic get fieldDynamic;
35+
int get fieldInt;
36+
T get fieldT;
37+
S get fieldS;
38+
Map<String, dynamic> toJson() => new _$GenericClassJsonMapWrapper<T, S>(this);
39+
}
40+
41+
class _$GenericClassJsonMapWrapper<T extends num, S> extends $JsonMapWrapper {
42+
final _$GenericClassSerializerMixin<T, S> _v;
43+
_$GenericClassJsonMapWrapper(this._v);
44+
45+
@override
46+
Iterable<String> get keys =>
47+
const ['fieldObject', 'fieldDynamic', 'fieldInt', 'fieldT', 'fieldS'];
48+
49+
@override
50+
dynamic operator [](Object key) {
51+
if (key is String) {
52+
switch (key) {
53+
case 'fieldObject':
54+
return _v.fieldObject == null ? null : _dataToJson(_v.fieldObject);
55+
case 'fieldDynamic':
56+
return _v.fieldDynamic == null ? null : _dataToJson(_v.fieldDynamic);
57+
case 'fieldInt':
58+
return _v.fieldInt == null ? null : _dataToJson(_v.fieldInt);
59+
case 'fieldT':
60+
return _v.fieldT == null ? null : _dataToJson(_v.fieldT);
61+
case 'fieldS':
62+
return _v.fieldS == null ? null : _dataToJson(_v.fieldS);
63+
}
64+
}
65+
return null;
66+
}
67+
}

0 commit comments

Comments
 (0)