Skip to content

Commit d4edbe5

Browse files
committed
Add fromJsonFunction and toJsonFunction fields to JsonKey class
Closes #137
1 parent d3747ad commit d4edbe5

22 files changed

+376
-24
lines changed

json_annotation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.4
2+
3+
* Added `fromJsonFunction` and `toJsonFunction` fields to `JsonKey` class.
4+
15
## 0.2.3
26

37
* Added `ignore` field to `JsonKey` class annotation

json_annotation/lib/src/json_serializable.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,24 @@ class JsonKey {
7272
/// serialization.
7373
final bool ignore;
7474

75+
/// A top-level [Function] to use when deserializing the associated JSON
76+
/// value to the annotated field.
77+
final Function fromJsonFunction;
78+
79+
/// A top-level [Function] to use when serializing the annotated field to
80+
/// JSON.
81+
final Function toJsonFunction;
82+
7583
/// Creates a new [JsonKey].
7684
///
7785
/// Only required when the default behavior is not desired.
78-
const JsonKey({this.name, this.nullable, this.includeIfNull, this.ignore});
86+
const JsonKey(
87+
{this.name,
88+
this.nullable,
89+
this.includeIfNull,
90+
this.ignore,
91+
this.fromJsonFunction,
92+
this.toJsonFunction});
7993
}
8094

8195
/// Helper classes used in generated code when

json_annotation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_annotation
2-
version: 0.2.3
2+
version: 0.2.4-dev
33
description: Annotations for the json_serializable package
44
homepage: https://github.com/dart-lang/json_serializable
55
author: Dart Team <misc@dartlang.org>

json_serializable/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
## 0.5.1
22

3+
* Support new `fromJsonFunction` and `toJsonFunction` fields on `JsonKey`.
4+
35
* Use `log` exposed by `package:build`. This requires end-users to have at least
46
`package:build_runner` `^0.8.2`.
7+
58
* Updated minimum `package:source_gen` dependency to `0.8.1` which includes
69
improved error messages.
710

json_serializable/lib/src/generator_helper.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
301301
}
302302

303303
TypeHelperContext _getHelperContext(FieldElement field) =>
304-
new TypeHelperContext(_generator, field.metadata, _nullable(field));
304+
new TypeHelperContext(
305+
_generator, field.metadata, _nullable(field), jsonKeyFor(field));
305306

306307
/// Returns `true` if the field can be written to JSON 'naively' – meaning
307308
/// we can avoid checking for `null`.

json_serializable/lib/src/json_key_helpers.dart

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:analyzer/analyzer.dart';
56
import 'package:analyzer/dart/element/element.dart';
7+
import 'package:analyzer/dart/element/type.dart';
8+
import 'package:analyzer/dart/constant/value.dart';
69

710
import 'package:json_annotation/json_annotation.dart';
11+
import 'package:meta/meta.dart';
812
import 'package:source_gen/source_gen.dart';
913

10-
final _jsonKeyExpando = new Expando<JsonKey>();
14+
@alwaysThrows
15+
T _throwUnsupported<T>(FieldElement element, String message, {String todo}) =>
16+
throw new InvalidGenerationSourceError(
17+
'Error with `@JsonKey` on `${element.displayName}`. $message',
18+
element: element,
19+
todo: todo);
20+
21+
final _jsonKeyExpando = new Expando<JsonKeyImpl>();
1122

1223
final _jsonKeyChecker = const TypeChecker.fromRuntime(JsonKey);
1324

14-
JsonKey jsonKeyFor(FieldElement element) {
25+
JsonKeyImpl jsonKeyFor(FieldElement element) {
1526
var key = _jsonKeyExpando[element];
1627

1728
if (key == null) {
@@ -21,14 +32,108 @@ JsonKey jsonKeyFor(FieldElement element) {
2132
var obj = _jsonKeyChecker.firstAnnotationOfExact(element) ??
2233
_jsonKeyChecker.firstAnnotationOfExact(element.getter);
2334

24-
_jsonKeyExpando[element] = key = obj == null
25-
? const JsonKey()
26-
: new JsonKey(
27-
name: obj.getField('name').toStringValue(),
28-
nullable: obj.getField('nullable').toBoolValue(),
29-
includeIfNull: obj.getField('includeIfNull').toBoolValue(),
30-
ignore: obj.getField('ignore').toBoolValue());
35+
if (obj == null) {
36+
key = const JsonKeyImpl.empty();
37+
} else {
38+
key = _from(element, obj);
39+
}
40+
_jsonKeyExpando[element] = key;
3141
}
3242

3343
return key;
3444
}
45+
46+
JsonKeyImpl _from(FieldElement element, DartObject obj) {
47+
var fromJsonName = _getFunctionName(obj, element, true);
48+
var toJsonName = _getFunctionName(obj, element, false);
49+
50+
return new JsonKeyImpl(
51+
obj.getField('name').toStringValue(),
52+
obj.getField('nullable').toBoolValue(),
53+
obj.getField('includeIfNull').toBoolValue(),
54+
obj.getField('ignore').toBoolValue(),
55+
fromJsonName,
56+
toJsonName);
57+
}
58+
59+
class ConvertDataImpl {
60+
final String name;
61+
final DartType paramType;
62+
63+
ConvertDataImpl(this.name, this.paramType);
64+
}
65+
66+
class JsonKeyImpl extends JsonKey {
67+
final ConvertDataImpl fromJsonData;
68+
final ConvertDataImpl toJsonData;
69+
70+
const JsonKeyImpl.empty()
71+
: fromJsonData = null,
72+
toJsonData = null,
73+
super();
74+
75+
JsonKeyImpl(String name, bool nullable, bool includeIfNull, bool ignore,
76+
this.fromJsonData, this.toJsonData)
77+
: super(
78+
name: name,
79+
nullable: nullable,
80+
includeIfNull: includeIfNull,
81+
ignore: ignore);
82+
}
83+
84+
ConvertDataImpl _getFunctionName(
85+
DartObject obj, FieldElement element, bool isFrom) {
86+
DartObject objectValue;
87+
if (isFrom) {
88+
objectValue = obj.getField('fromJsonFunction');
89+
} else {
90+
objectValue = obj.getField('toJsonFunction');
91+
}
92+
93+
if (objectValue.isNull) {
94+
return null;
95+
}
96+
97+
var type = objectValue.type as FunctionType;
98+
99+
if (type.element is MethodElement) {
100+
_throwUnsupported(
101+
element,
102+
'The function provided for `convert` must be top-level. '
103+
'Static class methods (`${type.element.name}`) are not supported.');
104+
}
105+
var functionElement = type.element as FunctionElement;
106+
107+
var positionalParams = functionElement.parameters
108+
.where((pe) => pe.parameterKind == ParameterKind.REQUIRED)
109+
.toList();
110+
111+
if (positionalParams.length != 1) {
112+
_throwUnsupported(
113+
element,
114+
'The convert function `${functionElement.name}` must have one '
115+
'positional paramater.');
116+
}
117+
118+
if (isFrom) {
119+
if (!functionElement.returnType.isAssignableTo(element.type)) {
120+
_throwUnsupported(
121+
element,
122+
'The convert function `${functionElement.name}` return type '
123+
'`${functionElement
124+
.returnType}` is not compatible with field type'
125+
' `${element.type}`.');
126+
}
127+
} else {
128+
var argType = positionalParams.single.type;
129+
if (!element.type.isAssignableTo(argType)) {
130+
_throwUnsupported(
131+
element,
132+
'The convert function `${functionElement.name}` argument type '
133+
'`$argType` is not compatible with field type'
134+
' `${element.type}`.');
135+
}
136+
}
137+
return new ConvertDataImpl(
138+
functionElement.name, positionalParams.single.type);
139+
}

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:source_gen/source_gen.dart';
1111

1212
import 'generator_helper.dart';
1313
import 'type_helper.dart';
14+
import 'type_helpers/convert_helper.dart';
1415
import 'type_helpers/date_time_helper.dart';
1516
import 'type_helpers/enum_helper.dart';
1617
import 'type_helpers/iterable_helper.dart';
@@ -24,6 +25,7 @@ Iterable<TypeHelper> allHelpersImpl(JsonSerializableGenerator generator) =>
2425
class JsonSerializableGenerator
2526
extends GeneratorForAnnotation<JsonSerializable> {
2627
static const _coreHelpers = const [
28+
const ConvertHelper(),
2729
const IterableHelper(),
2830
const MapHelper(),
2931
const EnumHelper(),

json_serializable/lib/src/type_helper_context.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:analyzer/dart/element/element.dart';
66
import 'package:analyzer/dart/element/type.dart';
77

8+
import 'json_key_helpers.dart';
89
import 'json_serializable_generator.dart';
910
import 'type_helper.dart';
1011

@@ -20,7 +21,10 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {
2021
@override
2122
final bool nullable;
2223

23-
TypeHelperContext(this._generator, this.metadata, this.nullable);
24+
final JsonKeyImpl jsonKey;
25+
26+
TypeHelperContext(
27+
this._generator, this.metadata, this.nullable, this.jsonKey);
2428

2529
@override
2630
String serialize(DartType targetType, String expression) => _run(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
import 'package:analyzer/dart/element/type.dart';
6+
7+
import '../type_helper.dart';
8+
import '../type_helper_context.dart';
9+
import '../utils.dart';
10+
11+
class ConvertHelper extends TypeHelper {
12+
const ConvertHelper();
13+
14+
@override
15+
String serialize(
16+
DartType targetType, String expression, SerializeContext context) {
17+
var jsonKey = (context as TypeHelperContext).jsonKey;
18+
if (jsonKey.toJsonData != null) {
19+
assert(targetType.isAssignableTo(jsonKey.toJsonData.paramType));
20+
21+
var result = '${jsonKey.toJsonData.name}($expression)';
22+
return commonNullPrefix(context.nullable, expression, result);
23+
}
24+
return null;
25+
}
26+
27+
@override
28+
String deserialize(
29+
DartType targetType, String expression, DeserializeContext context) {
30+
var jsonKey = (context as TypeHelperContext).jsonKey;
31+
if (jsonKey.fromJsonData != null) {
32+
var asContent = '';
33+
var paramType = jsonKey.fromJsonData.paramType;
34+
if (!(paramType.isDynamic || paramType.isObject)) {
35+
asContent = ' as $paramType';
36+
}
37+
var result = '${jsonKey.fromJsonData.name}($expression$asContent)';
38+
return commonNullPrefix(context.nullable, expression, result);
39+
}
40+
return null;
41+
}
42+
}

json_serializable/pubspec.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies:
1212

1313
# Use a tight version constraint to ensure that a constraint on
1414
# `json_annotation`. Properly constrains all features it provides.
15-
json_annotation: '>=0.2.3 <0.2.4'
15+
json_annotation: '>=0.2.4 <0.2.5'
1616
path: ^1.3.2
1717
source_gen: '>=0.8.1 <0.9.0'
1818
dev_dependencies:
@@ -21,3 +21,7 @@ dev_dependencies:
2121
collection: ^1.14.0
2222
dart_style: ^1.0.0
2323
test: ^0.12.3
24+
25+
dependency_overrides:
26+
json_annotation:
27+
path: ../json_annotation

json_serializable/test/json_serializable_integration_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ void main() {
128128
..nums = [0, 0.0]
129129
..doubles = [0.0]
130130
..nnDoubles = [0.0]
131-
..ints = [0]);
131+
..ints = [0]
132+
..duration = const Duration(seconds: 1));
132133
});
133134

134135
test('support ints as doubles', () {

json_serializable/test/json_serializable_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,67 @@ abstract class _$OrderSerializerMixin {
309309
}
310310
});
311311

312+
group('functions', () {
313+
group('fromJsonFunction', () {
314+
test('with bad fromJson return type', () {
315+
expectThrows(
316+
'BadFromFuncReturnType',
317+
'Error with `@JsonKey` on `field`. The convert function `_toInt` '
318+
'return type `int` is not compatible with field type `String`.');
319+
});
320+
test('with 2 arg fromJson function', () {
321+
expectThrows(
322+
'InvalidFromFunc2Args',
323+
'Error with `@JsonKey` on `field`. The convert function '
324+
'`_twoArgFunction` must have one positional paramater.');
325+
});
326+
test('with class static function', () {
327+
expectThrows(
328+
'InvalidFromFuncClassStatic',
329+
'Error with `@JsonKey` on `field`. '
330+
'The function provided for `convert` must be top-level. '
331+
'Static class methods (`_staticFunc`) are not supported.');
332+
});
333+
});
334+
335+
group('toJsonFunction', () {
336+
test('with bad fromJson return type', () {
337+
expectThrows(
338+
'BadToFuncReturnType',
339+
'Error with `@JsonKey` on `field`. The convert function `_toInt` '
340+
'argument type `bool` is not compatible with field type `String`.');
341+
});
342+
test('with 2 arg fromJson function', () {
343+
expectThrows(
344+
'InvalidToFunc2Args',
345+
'Error with `@JsonKey` on `field`. The convert function '
346+
'`_twoArgFunction` must have one positional paramater.');
347+
});
348+
test('with class static function', () {
349+
expectThrows(
350+
'InvalidToFuncClassStatic',
351+
'Error with `@JsonKey` on `field`. '
352+
'The function provided for `convert` must be top-level. '
353+
'Static class methods (`_staticFunc`) are not supported.');
354+
});
355+
});
356+
357+
if (!generator.useWrappers) {
358+
test('object', () async {
359+
var output = await runForElementNamed('ObjectConvertMethods');
360+
expect(output, contains("_toObject(json['field'])"));
361+
});
362+
test('dynamic', () async {
363+
var output = await runForElementNamed('DynamicConvertMethods');
364+
expect(output, contains("_toDynamic(json['field'])"));
365+
});
366+
test('typed', () async {
367+
var output = await runForElementNamed('TypedConvertMethods');
368+
expect(output, contains("_toString(json['field'] as String)"));
369+
});
370+
}
371+
});
372+
312373
test('missing default ctor with a factory', () async {
313374
expect(
314375
() => runForElementNamed('NoCtorClass'),

0 commit comments

Comments
 (0)