Skip to content

Commit 03fa181

Browse files
committed
Finish implementation of defaultValue
Implementation for `checked` option Improve error messages on unsupported types Explicitly disallow defaultValue + non-nullable field support serialization of single enum values Also added $enumDecode[Nullable] helpers to json_annotation
1 parent 5e74afc commit 03fa181

24 files changed

+378
-182
lines changed

json_annotation/CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
* Added `JsonKey.defaultValue`.
44

5+
* Added helpers for deserialization of `enum` values.
6+
These functions starting with `$` are referenced by generated code.
7+
They are not meant for direct use.
8+
59
## 0.2.5
610

711
* Added `CheckedFromJsonException` which is thrown by code generated when
812
`checked` is enabled in `json_serializable`.
913

10-
* Added functions to support the `checked` generation option. These
11-
functions start with `$` are referenced by generated code. They are not meant
12-
for direct use.
14+
* Added functions to support the `checked` generation option.
15+
These functions starting with `$` are referenced by generated code.
16+
They are not meant for direct use.
1317

1418
## 0.2.4
1519

json_annotation/lib/src/json_serializable.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,22 @@ class JsonKey {
133133
this.toJson,
134134
this.defaultValue});
135135
}
136+
137+
// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
138+
/// Helper class used in generated code with `enum` values.
139+
///
140+
/// Should not be used directly.
141+
T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
142+
values.singleWhere((e) => e.toString() == '$enumName.$enumValue',
143+
orElse: () => throw new ArgumentError(
144+
'`$enumValue` is not one of the supported values: '
145+
'${values.map(_nameForEnumValue).join(', ')}'));
146+
147+
/// Helper class used in generated code with nullable `enum` values.
148+
///
149+
/// Should not be used directly.
150+
T $enumDecodeNullable<T>(String enumName, List<T> values, String enumValue) =>
151+
enumValue == null ? null : $enumDecode(enumName, values, enumValue);
152+
153+
// Until enum has a name property: github.com/dart-lang/sdk/issues/21712
154+
String _nameForEnumValue(Object value) => value.toString().split('.')[1];

json_serializable/CHANGELOG.md

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

33
* Added support for `JsonKey.defaultValue`.
44

5+
* `enum` deserialization now uses helpers provided by `json_annotation`.
6+
57
* Small change to how nullable `Map` values are deserialized.
68

79
* Small whitespace changes to `JsonLiteral` generation to align with `dartfmt`.

json_serializable/lib/src/generator_helper.dart

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,34 +382,37 @@ class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
382382
String _deserializeForField(FieldElement field,
383383
{ParameterElement ctorParam, bool checkedProperty}) {
384384
checkedProperty ??= false;
385-
var defaultValue = jsonKeyFor(field).defaultValue;
386385
var jsonKeyName = _safeNameAccess(field);
387-
388386
var targetType = ctorParam?.type ?? field.type;
387+
var contextHelper = _getHelperContext(field);
389388

389+
String value;
390390
try {
391391
if (_generator.checked) {
392-
// TODO: default value fun here!
393-
var value = _getHelperContext(field).deserialize(targetType, 'v');
394-
if (checkedProperty) {
395-
return value;
392+
value = contextHelper.deserialize(targetType, 'v');
393+
if (!checkedProperty) {
394+
value = '\$checkedConvert(json, $jsonKeyName, (v) => $value)';
396395
}
396+
} else {
397+
assert(!checkedProperty,
398+
'should only be true if `_generator.checked` is true.');
397399

398-
return '\$checkedConvert(json, $jsonKeyName, (v) => $value)';
399-
}
400-
assert(!checkedProperty,
401-
'should only be true if `_generator.checked` is true.');
402-
403-
var value = _getHelperContext(field)
404-
.deserialize(targetType, 'json[$jsonKeyName]');
405-
406-
if (defaultValue != null) {
407-
value = '$value ?? $defaultValue';
400+
value = contextHelper.deserialize(targetType, 'json[$jsonKeyName]');
408401
}
409-
return value;
410402
} on UnsupportedTypeError catch (e) {
411403
throw _createInvalidGenerationError('fromJson', field, e);
412404
}
405+
406+
var defaultValue = jsonKeyFor(field).defaultValue;
407+
if (defaultValue != null) {
408+
if (!contextHelper.nullable) {
409+
throwUnsupported(field,
410+
'Cannot use `defaultValue` on a field with `nullable` false.');
411+
}
412+
413+
value = '$value ?? $defaultValue';
414+
}
415+
return value;
413416
}
414417

415418
TypeHelperContext _getHelperContext(FieldElement field) {

json_serializable/lib/src/json_key_helpers.dart

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import 'package:meta/meta.dart' show alwaysThrows;
1111
import 'package:source_gen/source_gen.dart';
1212

1313
import 'json_literal_generator.dart';
14+
import 'utils.dart';
1415

1516
@alwaysThrows
16-
T _throwUnsupported<T>(FieldElement element, String message) =>
17+
void throwUnsupported(FieldElement element, String message) =>
1718
throw new InvalidGenerationSourceError(
1819
'Error with `@JsonKey` on `${element.name}`. $message',
1920
element: element);
@@ -45,41 +46,71 @@ JsonKeyWithConversion _from(FieldElement element) {
4546
var fromJsonName = _getFunctionName(obj, element, true);
4647
var toJsonName = _getFunctionName(obj, element, false);
4748

48-
Object _getLiteral(DartObject dartObject) {
49+
Object _getLiteral(DartObject dartObject, Iterable<String> things) {
4950
if (dartObject.isNull) {
5051
return null;
5152
}
5253

5354
var reader = new ConstantReader(dartObject);
5455

56+
String badType;
5557
if (reader.isSymbol) {
56-
_throwUnsupported(element,
57-
'Values of type `Symbol` are not supported for `defaultValue`.');
58+
badType = 'Symbol';
5859
} else if (reader.isType) {
59-
_throwUnsupported(element,
60-
'Values of type `Type` are not supported for `defaultValue`.');
60+
badType = 'Type';
61+
} else if (dartObject.type is FunctionType) {
62+
// TODO(kevmoo): Support calling function for the default value?
63+
badType = 'Function';
6164
} else if (!reader.isLiteral) {
62-
_throwUnsupported(
63-
element, 'The provided `defaultValue` is not a literal: $dartObject');
65+
badType = dartObject.type.name;
66+
}
67+
68+
if (badType != null) {
69+
badType = things.followedBy([badType]).join(' > ');
70+
throwUnsupported(
71+
element, '`defaultValue` is `$badType`, it must be a literal.');
6472
}
6573

6674
var literal = reader.literalValue;
75+
6776
if (literal is num || literal is String || literal is bool) {
6877
return literal;
6978
} else if (literal is List<DartObject>) {
70-
return literal.map(_getLiteral).toList();
71-
} else if (literal is Map<DartObject, DartObject>) {
7279
return literal
73-
.map((k, v) => new MapEntry(_getLiteral(k), _getLiteral(v)));
80+
.map((e) => _getLiteral(e, things.followedBy(['List'])))
81+
.toList();
82+
} else if (literal is Map<DartObject, DartObject>) {
83+
var mapThings = things.followedBy(['Map']);
84+
return literal.map((k, v) =>
85+
new MapEntry(_getLiteral(k, mapThings), _getLiteral(v, mapThings)));
7486
}
75-
_throwUnsupported(
76-
element, 'The provided value is not supported: $dartObject');
77-
}
7887

79-
var defaultValueLiteral = _getLiteral(obj.getField('defaultValue'));
88+
badType = things.followedBy(['$dartObject']).join(' > ');
8089

81-
if (defaultValueLiteral != null) {
82-
defaultValueLiteral = jsonLiteralAsDart(defaultValueLiteral, false);
90+
throwUnsupported(
91+
element,
92+
'The provided value is not supported: $badType. '
93+
'This may be an error in package:json_serializable. '
94+
'Please rerun your build with `--verbose` and file an issue.');
95+
}
96+
97+
var defaultValueObject = obj.getField('defaultValue');
98+
99+
Object defaultValueLiteral;
100+
if (isEnum(defaultValueObject.type)) {
101+
var interfaceType = defaultValueObject.type as InterfaceType;
102+
var allowedValues = interfaceType.accessors
103+
.where((p) => p.returnType == interfaceType)
104+
.map((p) => p.name)
105+
.toList();
106+
var enumValueIndex = defaultValueObject.getField('index').toIntValue();
107+
defaultValueLiteral =
108+
'${interfaceType.name}.${allowedValues[enumValueIndex]}';
109+
} else {
110+
defaultValueLiteral = _getLiteral(defaultValueObject, []);
111+
if (defaultValueLiteral != null) {
112+
defaultValueLiteral = jsonLiteralAsDart(defaultValueLiteral, false);
113+
}
83114
}
84115

85116
return new JsonKeyWithConversion._(
@@ -130,7 +161,7 @@ ConvertData _getFunctionName(
130161
var type = objectValue.type as FunctionType;
131162

132163
if (type.element is MethodElement) {
133-
_throwUnsupported(
164+
throwUnsupported(
134165
element,
135166
'The function provided for `$paramName` must be top-level. '
136167
'Static class methods (`${type.element.name}`) are not supported.');
@@ -140,7 +171,7 @@ ConvertData _getFunctionName(
140171
if (functionElement.parameters.isEmpty ||
141172
functionElement.parameters.first.isNamed ||
142173
functionElement.parameters.where((pe) => !pe.isOptional).length > 1) {
143-
_throwUnsupported(
174+
throwUnsupported(
144175
element,
145176
'The `$paramName` function `${functionElement.name}` must have one '
146177
'positional paramater.');
@@ -155,7 +186,7 @@ ConvertData _getFunctionName(
155186
// to the `fromJson` function.
156187
// TODO: consider adding error checking here if there is confusion.
157188
} else if (!returnType.isAssignableTo(element.type)) {
158-
_throwUnsupported(
189+
throwUnsupported(
159190
element,
160191
'The `$paramName` function `${functionElement.name}` return type '
161192
'`$returnType` is not compatible with field type `${element.type}`.');
@@ -166,7 +197,7 @@ ConvertData _getFunctionName(
166197
// to the `fromJson` function.
167198
// TODO: consider adding error checking here if there is confusion.
168199
} else if (!element.type.isAssignableTo(argType)) {
169-
_throwUnsupported(
200+
throwUnsupported(
170201
element,
171202
'The `$paramName` function `${functionElement.name}` argument type '
172203
'`$argType` is not compatible with field type'

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'package:analyzer/dart/element/type.dart';
66

7-
import '../constants.dart' as consts;
87
import '../type_helper.dart';
98
import '../utils.dart';
109

@@ -31,18 +30,9 @@ class EnumHelper extends TypeHelper {
3130
return null;
3231
}
3332

34-
var wrappedExpression =
35-
simpleExpression.hasMatch(expression) ? expression : '{$expression}';
36-
37-
var closureArg = consts.closureArg;
38-
if (closureArg == wrappedExpression) {
39-
closureArg = '${closureArg}2';
40-
}
41-
42-
return commonNullPrefix(
43-
context.nullable,
44-
expression,
45-
'$targetType.values.singleWhere(($closureArg) => $closureArg.toString()'
46-
" == '$targetType.\$$wrappedExpression')");
33+
var functionName =
34+
context.nullable ? r'$enumDecodeNullable' : r'$enumDecode';
35+
return "$functionName('$targetType', $targetType.values, "
36+
'$expression as String)';
4737
}
4838
}

json_serializable/test/default_value/default_value.non_nullable.dart renamed to json_serializable/test/default_value/default_value.checked.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@
55
// GENERATED CODE - DO NOT MODIFY BY HAND
66

77
// **************************************************************************
8-
// Generator: _NonNullableGenerator
8+
// Generator: _CheckedGenerator
99
// **************************************************************************
1010

1111
// ignore_for_file: annotate_overrides
1212

1313
import 'package:json_annotation/json_annotation.dart';
1414

15-
part 'default_value.non_nullable.g.dart';
15+
import 'default_value_interface.dart' as dvi hide Greek;
16+
import 'default_value_interface.dart' show Greek;
17+
18+
part 'default_value.checked.g.dart';
1619

1720
const _intValue = 42;
1821

19-
@JsonSerializable(nullable: false)
20-
class DefaultValue extends Object with _$DefaultValueSerializerMixin {
22+
dvi.DefaultValue fromJson(Map<String, dynamic> json) =>
23+
_$DefaultValueFromJson(json);
24+
25+
@JsonSerializable()
26+
class DefaultValue extends Object
27+
with _$DefaultValueSerializerMixin
28+
implements dvi.DefaultValue {
2129
@JsonKey(defaultValue: true)
2230
bool fieldBool;
2331

@@ -47,6 +55,9 @@ class DefaultValue extends Object with _$DefaultValueSerializerMixin {
4755
})
4856
Map<String, List<String>> fieldMapListString;
4957

58+
@JsonKey(defaultValue: Greek.beta)
59+
Greek fieldEnum;
60+
5061
DefaultValue();
5162

5263
factory DefaultValue.fromJson(Map<String, dynamic> json) =>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
// GENERATED CODE - DO NOT MODIFY BY HAND
6+
7+
part of 'default_value.checked.dart';
8+
9+
// **************************************************************************
10+
// Generator: JsonSerializableGenerator
11+
// **************************************************************************
12+
13+
DefaultValue _$DefaultValueFromJson(Map json) =>
14+
$checkedNew('DefaultValue', json, () {
15+
var val = new DefaultValue();
16+
17+
$checkedConvert(
18+
json, 'fieldBool', (v) => val.fieldBool = v as bool ?? true);
19+
$checkedConvert(json, 'fieldString',
20+
(v) => val.fieldString = v as String ?? 'string');
21+
$checkedConvert(json, 'fieldInt', (v) => val.fieldInt = v as int ?? 42);
22+
$checkedConvert(json, 'fieldDouble',
23+
(v) => val.fieldDouble = (v as num)?.toDouble() ?? 3.14);
24+
$checkedConvert(
25+
json, 'fieldListEmpty', (v) => val.fieldListEmpty = v as List ?? []);
26+
$checkedConvert(
27+
json, 'fieldMapEmpty', (v) => val.fieldMapEmpty = v as Map ?? {});
28+
$checkedConvert(
29+
json,
30+
'fieldListSimple',
31+
(v) => val.fieldListSimple =
32+
(v as List)?.map((e) => e as int)?.toList() ?? [1, 2, 3]);
33+
$checkedConvert(
34+
json,
35+
'fieldMapSimple',
36+
(v) => val.fieldMapSimple =
37+
(v as Map)?.map((k, e) => new MapEntry(k as String, e as int)) ??
38+
{'answer': 42});
39+
$checkedConvert(
40+
json,
41+
'fieldMapListString',
42+
(v) => val.fieldMapListString = (v as Map)?.map((k, e) =>
43+
new MapEntry(k as String,
44+
(e as List)?.map((e) => e as String)?.toList())) ??
45+
{
46+
'root': ['child']
47+
});
48+
$checkedConvert(
49+
json,
50+
'fieldEnum',
51+
(v) => val.fieldEnum =
52+
$enumDecodeNullable('Greek', Greek.values, v as String) ??
53+
Greek.beta);
54+
return val;
55+
});
56+
57+
abstract class _$DefaultValueSerializerMixin {
58+
bool get fieldBool;
59+
String get fieldString;
60+
int get fieldInt;
61+
double get fieldDouble;
62+
List<dynamic> get fieldListEmpty;
63+
Map<dynamic, dynamic> get fieldMapEmpty;
64+
List<int> get fieldListSimple;
65+
Map<String, int> get fieldMapSimple;
66+
Map<String, List<String>> get fieldMapListString;
67+
Greek get fieldEnum;
68+
Map<String, dynamic> toJson() => <String, dynamic>{
69+
'fieldBool': fieldBool,
70+
'fieldString': fieldString,
71+
'fieldInt': fieldInt,
72+
'fieldDouble': fieldDouble,
73+
'fieldListEmpty': fieldListEmpty,
74+
'fieldMapEmpty': fieldMapEmpty,
75+
'fieldListSimple': fieldListSimple,
76+
'fieldMapSimple': fieldMapSimple,
77+
'fieldMapListString': fieldMapListString,
78+
'fieldEnum':
79+
fieldEnum == null ? null : fieldEnum.toString().split('.')[1]
80+
};
81+
}

0 commit comments

Comments
 (0)