Skip to content

Add support for generic classes #169

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

Merged
merged 2 commits into from
May 22, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
* Add `checked` configuration option. If `true`, generated `fromJson` functions
include extra checks to validate proper deserialization of types.

* Use `Map.map` for more map conversions. Simplifies generated code and fixes
a subtle issue when the `Map` key type is `dynamic` or `Object`.

* Added `any_map` to configuration. Allows `fromJson` code to
support dynamic `Map` instances that are not explicitly
`Map<String, dynaimc>`.

* Added support for classes with type arguments.

* Use `Map.map` for more map conversions. Simplifies generated code and fixes
a subtle issue when the `Map` key type is `dynamic` or `Object`.

## 0.5.3

* Require the latest version of `package:analyzer` - `v0.32.0`.
Expand Down
2 changes: 1 addition & 1 deletion json_serializable/example/example.g.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

Expand Down
32 changes: 24 additions & 8 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,22 @@ class _GeneratorHelper {
final StringBuffer _buffer = new StringBuffer();

String get _prefix => '_\$${_element.name}';
String get _mixClassName => '${_prefix}SerializerMixin';
String get _helpClassName => '${_prefix}JsonMapWrapper';

String _mixinClassName(bool withConstraints) =>
'${_prefix}SerializerMixin${_genericClassArguments(withConstraints)}';

String _wrapperClassName([bool withConstraints]) =>
'${_prefix}JsonMapWrapper${_genericClassArguments(withConstraints)}';

/// Returns a [String] representing the type arguments that exist on
/// [_element].
///
/// Returns the output of calling [genericClassArguments] with [_element].
String _genericClassArguments(bool withConstraints) =>
genericClassArguments(_element, withConstraints);

String get _targetClassReference =>
'${_element.name}${_genericClassArguments(false)}';

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

Expand All @@ -66,7 +80,7 @@ class _GeneratorHelper {
//
// Generate the mixin class
//
_buffer.writeln('abstract class $_mixClassName {');
_buffer.writeln('abstract class ${_mixinClassName(true)} {');

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

if (_generator.useWrappers) {
_buffer.writeln('=> new $_helpClassName(this);');
_buffer.writeln('=> new ${_wrapperClassName(false)}(this);');
} else {
if (writeNaive) {
// write simple `toJson` method that includes all keys...
Expand Down Expand Up @@ -126,7 +140,8 @@ class _GeneratorHelper {
if (_annotation.createFactory) {
_buffer.writeln();
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
_buffer.writeln('${_element.name} ${_prefix}FromJson($mapType json) =>');
_buffer.writeln('$_targetClassReference '
'${_prefix}FromJson${_genericClassArguments(true)}($mapType json) =>');

String deserializeFun(String paramOrFieldName,
{ParameterElement ctorParam}) =>
Expand Down Expand Up @@ -221,9 +236,10 @@ class _GeneratorHelper {
void _writeWrapper(Iterable<FieldElement> fields) {
_buffer.writeln();
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
_buffer.writeln('''class $_helpClassName extends \$JsonMapWrapper {
final $_mixClassName _v;
$_helpClassName(this._v);
_buffer
.writeln('''class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with writeln you're going to get an extra newline and this already ends in a newline. Do we need to?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, dartfmt handles most of this. Not going to modify this in this CL

final ${_mixinClassName(false)} _v;
${_wrapperClassName()}(this._v);
''');

if (fields.every(_writeJsonValueNaive)) {
Expand Down
13 changes: 11 additions & 2 deletions json_serializable/lib/src/json_key_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,23 @@ ConvertData _getFunctionName(
var argType = functionElement.parameters.first.type;
if (isFrom) {
var returnType = functionElement.returnType;
if (!returnType.isAssignableTo(element.type)) {

if (returnType is TypeParameterType) {
// We keep things simple in this case. We rely on inferred type arguments
// to the `fromJson` function.
// TODO: consider adding error checking here if there is confusion.
} else if (!returnType.isAssignableTo(element.type)) {
_throwUnsupported(
element,
'The `$paramName` function `${functionElement.name}` return type '
'`$returnType` is not compatible with field type `${element.type}`.');
}
} else {
if (!element.type.isAssignableTo(argType)) {
if (argType is TypeParameterType) {
// We keep things simple in this case. We rely on inferred type arguments
// to the `fromJson` function.
// TODO: consider adding error checking here if there is confusion.
} else if (!element.type.isAssignableTo(argType)) {
_throwUnsupported(
element,
'The `$paramName` function `${functionElement.name}` argument type '
Expand Down
4 changes: 2 additions & 2 deletions json_serializable/lib/src/type_helpers/convert_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class ConvertHelper extends TypeHelper {
DartType targetType, String expression, SerializeContext context) {
var toJsonData = (context as TypeHelperContext).toJsonData;
if (toJsonData != null) {
assert(targetType.isAssignableTo(toJsonData.paramType));

assert(toJsonData.paramType is TypeParameterType ||
targetType.isAssignableTo(toJsonData.paramType));
var result = '${toJsonData.name}($expression)';
return commonNullPrefix(context.nullable, expression, result);
}
Expand Down
35 changes: 33 additions & 2 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,38 @@ int _sortByLocation(FieldElement a, FieldElement b) {

final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);

/// If a parameter is required to invoke the constructor,
/// Returns a [String] representing the type arguments that exist on
/// [element].
///
/// If [withConstraints] is `null` or if [element] has no type arguments, an
/// empty [String] is returned.
///
/// If [withConstraints] is true, any type constraints that exist on [element]
/// are included.
///
/// For example, for class `class Sample<T as num, S>{...}`
///
/// For [withConstraints] = `false`:
///
/// ```
/// "<T, S>"
/// ```
///
/// For [withConstraints] = `true`:
///
/// ```
/// "<T as num, S>"
/// ```
String genericClassArguments(ClassElement element, bool withConstraints) {
if (withConstraints == null || element.typeParameters.isEmpty) {
return '';
}
var values = element.typeParameters
.map((t) => withConstraints ? t.toString() : t.name)
.join(',');
return '<$values>';
}

/// [availableConstructorParameters] is checked to see if it is available. If
/// [availableConstructorParameters] does not contain the parameter name,
/// an [UnsupportedError] is thrown.
Expand Down Expand Up @@ -243,7 +274,7 @@ CtorData writeConstructorInvocation(
writeableFields.toSet().difference(usedCtorParamsAndFields);

var buffer = new StringBuffer();
buffer.write('new $className(');
buffer.write('new $className${genericClassArguments(classElement, false)}(');
buffer.writeAll(
constructorArguments.map((paramElement) =>
deserializeForField(paramElement.name, ctorParam: paramElement)),
Expand Down
39 changes: 39 additions & 0 deletions json_serializable/test/generic_files/generic_class.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: annotate_overrides

import 'package:json_annotation/json_annotation.dart';

part 'generic_class.g.dart';

@JsonSerializable()
class GenericClass<T extends num, S> extends Object
with _$GenericClassSerializerMixin<T, S> {
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are _dataFromJson and _dataToJson required for this to work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Will document as such.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object fieldObject;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
dynamic fieldDynamic;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
int fieldInt;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
T fieldT;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
S fieldS;

GenericClass();

factory GenericClass.fromJson(Map<String, dynamic> json) =>
_$GenericClassFromJson<T, S>(json);
}

T _dataFromJson<T, S, U>(Map<String, dynamic> input, [S other1, U, other2]) =>
input['value'] as T;

Map<String, dynamic> _dataToJson<T, S, U>(T input, [S other1, U, other2]) =>
{'value': input};
45 changes: 45 additions & 0 deletions json_serializable/test/generic_files/generic_class.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'generic_class.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

GenericClass<T, S> _$GenericClassFromJson<T extends num, S>(
Map<String, dynamic> json) =>
new GenericClass<T, S>()
..fieldObject = json['fieldObject'] == null
? null
: _dataFromJson(json['fieldObject'] as Map<String, dynamic>)
..fieldDynamic = json['fieldDynamic'] == null
? null
: _dataFromJson(json['fieldDynamic'] as Map<String, dynamic>)
..fieldInt = json['fieldInt'] == null
? null
: _dataFromJson(json['fieldInt'] as Map<String, dynamic>)
..fieldT = json['fieldT'] == null
? null
: _dataFromJson(json['fieldT'] as Map<String, dynamic>)
..fieldS = json['fieldS'] == null
? null
: _dataFromJson(json['fieldS'] as Map<String, dynamic>);

abstract class _$GenericClassSerializerMixin<T extends num, S> {
Object get fieldObject;
dynamic get fieldDynamic;
int get fieldInt;
T get fieldT;
S get fieldS;
Map<String, dynamic> toJson() => <String, dynamic>{
'fieldObject': fieldObject == null ? null : _dataToJson(fieldObject),
'fieldDynamic': fieldDynamic == null ? null : _dataToJson(fieldDynamic),
'fieldInt': fieldInt == null ? null : _dataToJson(fieldInt),
'fieldT': fieldT == null ? null : _dataToJson(fieldT),
'fieldS': fieldS == null ? null : _dataToJson(fieldS)
};
}
45 changes: 45 additions & 0 deletions json_serializable/test/generic_files/generic_class.wrapped.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// Generator: _WrappedGenerator
// **************************************************************************

// ignore_for_file: annotate_overrides

import 'package:json_annotation/json_annotation.dart';

part 'generic_class.wrapped.g.dart';

@JsonSerializable()
class GenericClass<T extends num, S> extends Object
with _$GenericClassSerializerMixin<T, S> {
@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
Object fieldObject;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
dynamic fieldDynamic;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
int fieldInt;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
T fieldT;

@JsonKey(fromJson: _dataFromJson, toJson: _dataToJson)
S fieldS;

GenericClass();

factory GenericClass.fromJson(Map<String, dynamic> json) =>
_$GenericClassFromJson<T, S>(json);
}

T _dataFromJson<T, S, U>(Map<String, dynamic> input, [S other1, U, other2]) =>
input['value'] as T;

Map<String, dynamic> _dataToJson<T, S, U>(T input, [S other1, U, other2]) =>
{'value': input};
67 changes: 67 additions & 0 deletions json_serializable/test/generic_files/generic_class.wrapped.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'generic_class.wrapped.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

GenericClass<T, S> _$GenericClassFromJson<T extends num, S>(
Map<String, dynamic> json) =>
new GenericClass<T, S>()
..fieldObject = json['fieldObject'] == null
? null
: _dataFromJson(json['fieldObject'] as Map<String, dynamic>)
..fieldDynamic = json['fieldDynamic'] == null
? null
: _dataFromJson(json['fieldDynamic'] as Map<String, dynamic>)
..fieldInt = json['fieldInt'] == null
? null
: _dataFromJson(json['fieldInt'] as Map<String, dynamic>)
..fieldT = json['fieldT'] == null
? null
: _dataFromJson(json['fieldT'] as Map<String, dynamic>)
..fieldS = json['fieldS'] == null
? null
: _dataFromJson(json['fieldS'] as Map<String, dynamic>);

abstract class _$GenericClassSerializerMixin<T extends num, S> {
Object get fieldObject;
dynamic get fieldDynamic;
int get fieldInt;
T get fieldT;
S get fieldS;
Map<String, dynamic> toJson() => new _$GenericClassJsonMapWrapper<T, S>(this);
}

class _$GenericClassJsonMapWrapper<T extends num, S> extends $JsonMapWrapper {
final _$GenericClassSerializerMixin<T, S> _v;
_$GenericClassJsonMapWrapper(this._v);

@override
Iterable<String> get keys =>
const ['fieldObject', 'fieldDynamic', 'fieldInt', 'fieldT', 'fieldS'];

@override
dynamic operator [](Object key) {
if (key is String) {
switch (key) {
case 'fieldObject':
return _v.fieldObject == null ? null : _dataToJson(_v.fieldObject);
case 'fieldDynamic':
return _v.fieldDynamic == null ? null : _dataToJson(_v.fieldDynamic);
case 'fieldInt':
return _v.fieldInt == null ? null : _dataToJson(_v.fieldInt);
case 'fieldT':
return _v.fieldT == null ? null : _dataToJson(_v.fieldT);
case 'fieldS':
return _v.fieldS == null ? null : _dataToJson(_v.fieldS);
}
}
return null;
}
}
Loading