Skip to content

Commit 0268d4b

Browse files
committed
Add support for generic classes
Fixes #116
1 parent a99c427 commit 0268d4b

16 files changed

+461
-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: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,22 @@ 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+
43+
String _mixinClassName(bool withConstraints) =>
44+
'${_prefix}SerializerMixin${_genericClassArguments(withConstraints)}';
45+
46+
String _wrapperClassName([bool withConstraints]) =>
47+
'${_prefix}JsonMapWrapper${_genericClassArguments(withConstraints)}';
48+
49+
/// Returns a [String] representing the type arguments that exist on
50+
/// [_element].
51+
///
52+
/// Returns the output of calling [genericClassArguments] with [_element].
53+
String _genericClassArguments(bool withConstraints) =>
54+
genericClassArguments(_element, withConstraints);
55+
56+
String get _targetClassReference =>
57+
'${_element.name}${_genericClassArguments(false)}';
4458

4559
_GeneratorHelper(this._generator, this._element, this._annotation);
4660

@@ -66,7 +80,7 @@ class _GeneratorHelper {
6680
//
6781
// Generate the mixin class
6882
//
69-
_buffer.writeln('abstract class $_mixClassName {');
83+
_buffer.writeln('abstract class ${_mixinClassName(true)} {');
7084

7185
// write copies of the fields - this allows the toJson method to access
7286
// the fields of the target class
@@ -80,7 +94,7 @@ class _GeneratorHelper {
8094
var writeNaive = accessibleFields.every(_writeJsonValueNaive);
8195

8296
if (_generator.useWrappers) {
83-
_buffer.writeln('=> new $_helpClassName(this);');
97+
_buffer.writeln('=> new ${_wrapperClassName(false)}(this);');
8498
} else {
8599
if (writeNaive) {
86100
// write simple `toJson` method that includes all keys...
@@ -126,7 +140,8 @@ class _GeneratorHelper {
126140
if (_annotation.createFactory) {
127141
_buffer.writeln();
128142
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
129-
_buffer.writeln('${_element.name} ${_prefix}FromJson($mapType json) =>');
143+
_buffer.writeln('$_targetClassReference '
144+
'${_prefix}FromJson${_genericClassArguments(true)}($mapType json) =>');
130145

131146
String deserializeFun(String paramOrFieldName,
132147
{ParameterElement ctorParam}) =>
@@ -221,9 +236,10 @@ class _GeneratorHelper {
221236
void _writeWrapper(Iterable<FieldElement> fields) {
222237
_buffer.writeln();
223238
// 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);
239+
_buffer
240+
.writeln('''class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
241+
final ${_mixinClassName(false)} _v;
242+
${_wrapperClassName()}(this._v);
227243
''');
228244

229245
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: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,38 @@ 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+
/// Returns a [String] representing the type arguments that exist on
179+
/// [element].
180+
///
181+
/// If [withConstraints] is `null` or if [element] has no type arguments, an
182+
/// empty [String] is returned.
183+
///
184+
/// If [withConstraints] is true, any type constraints that exist on [element]
185+
/// are included.
186+
///
187+
/// For example, for class `class Sample<T as num, S>{...}`
188+
///
189+
/// For [withConstraints] = `false`:
190+
///
191+
/// ```
192+
/// "<T, S>"
193+
/// ```
194+
///
195+
/// For [withConstraints] = `true`:
196+
///
197+
/// ```
198+
/// "<T as num, S>"
199+
/// ```
200+
String genericClassArguments(ClassElement element, bool withConstraints) {
201+
if (withConstraints == null || element.typeParameters.isEmpty) {
202+
return '';
203+
}
204+
var values = element.typeParameters
205+
.map((t) => withConstraints ? t.toString() : t.name)
206+
.join(',');
207+
return '<$values>';
208+
}
209+
179210
/// [availableConstructorParameters] is checked to see if it is available. If
180211
/// [availableConstructorParameters] does not contain the parameter name,
181212
/// an [UnsupportedError] is thrown.
@@ -243,7 +274,7 @@ CtorData writeConstructorInvocation(
243274
writeableFields.toSet().difference(usedCtorParamsAndFields);
244275

245276
var buffer = new StringBuffer();
246-
buffer.write('new $className(');
277+
buffer.write('new $className${genericClassArguments(classElement, false)}(');
247278
buffer.writeAll(
248279
constructorArguments.map((paramElement) =>
249280
deserializeForField(paramElement.name, ctorParam: paramElement)),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2018, 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+
input['value'] as T;
37+
38+
Map<String, dynamic> _dataToJson<T, S, U>(T input, [S other1, U, other2]) =>
39+
{'value': input};
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)