Skip to content

Commit 4976682

Browse files
committed
Implementation
1 parent 187fa3d commit 4976682

9 files changed

+148
-50
lines changed

json_serializable/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 0.5.4
22

3+
* Add `checked` configuration option. If `true`, generated `fromJson` functions
4+
include extra checks to validate proper deserialization of types.
5+
36
* Use `Map.map` for more map conversions. Simplifies generated code and fixes
47
a subtle issue when the `Map` key type is `dynamic` or `Object`.
58

json_serializable/lib/json_serializable.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
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-
export 'src/json_literal_generator.dart';
5+
export 'src/json_literal_generator.dart' show JsonLiteralGenerator;
66
export 'src/json_serializable_generator.dart' show JsonSerializableGenerator;

json_serializable/lib/src/generator_helper.dart

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

1212
import 'constants.dart';
1313
import 'json_key_helpers.dart';
14+
import 'json_literal_generator.dart';
1415
import 'json_serializable_generator.dart';
1516
import 'type_helper.dart';
1617
import 'type_helper_context.dart';
@@ -126,16 +127,15 @@ class _GeneratorHelper {
126127
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
127128

128129
_buffer.writeln();
129-
_buffer.writeln('${_element.name} '
130-
'${_prefix}FromJson($mapType json) =>');
131130

132131
String deserializeFun(String paramOrFieldName,
133132
{ParameterElement ctorParam}) =>
134133
_deserializeForField(accessibleFields[paramOrFieldName],
135134
ctorParam: ctorParam);
136135

136+
var tempBuffer = new StringBuffer();
137137
var fieldsSetByFactory = writeConstructorInvocation(
138-
_buffer,
138+
tempBuffer,
139139
_element,
140140
accessibleFields.keys,
141141
accessibleFields.values
@@ -144,6 +144,21 @@ class _GeneratorHelper {
144144
.toList(),
145145
unavailableReasons,
146146
deserializeFun);
147+
148+
if (_generator.checked) {
149+
var keyFieldMap = new Map<String, String>.fromIterable(
150+
fieldsSetByFactory,
151+
value: (k) => _nameAccess(accessibleFields[k]));
152+
var mapLiteral = jsonMapAsDart(keyFieldMap, true);
153+
var classLiteral = escapeDartString(_element.displayName);
154+
155+
_buffer.writeln(
156+
'${_element.displayName} ${_prefix}FromJson($mapType json) =>'
157+
'\$checkedNew($classLiteral, json, $mapLiteral, ()=>$tempBuffer)');
158+
} else {
159+
_buffer.writeln('${_element.name} '
160+
'${_prefix}FromJson($mapType json) => $tempBuffer');
161+
}
147162
_buffer.writeln(';');
148163

149164
// If there are fields that are final – that are not set via the generated
@@ -297,7 +312,12 @@ void $toJsonMapHelperName(String key, dynamic value) {
297312
var targetType = ctorParam?.type ?? field.type;
298313

299314
try {
300-
return _getHelperContext(field).deserialize(targetType, 'json[$jsonKey]');
315+
var value =
316+
_getHelperContext(field).deserialize(targetType, 'json[$jsonKey]');
317+
if (_generator.checked) {
318+
return '\$checkedConvert(json, $jsonKey, () => $value)';
319+
}
320+
return value;
301321
} on UnsupportedTypeError catch (e) {
302322
throw _createInvalidGenerationError('fromJson', field, e);
303323
}

json_serializable/lib/src/json_literal_generator.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ String _jsonLiteralAsDart(dynamic value, bool asConst) {
5656
return '${asConst ? 'const' : ''}[$listItems]';
5757
}
5858

59-
if (value is Map<String, dynamic>) return _jsonMapAsDart(value, asConst);
59+
if (value is Map<String, dynamic>) return jsonMapAsDart(value, asConst);
6060

6161
throw new StateError(
6262
'Should never get here – with ${value.runtimeType} - `$value`.');
6363
}
6464

65-
String _jsonMapAsDart(Map<String, dynamic> value, bool asConst) {
65+
String jsonMapAsDart(Map<String, dynamic> value, bool asConst) {
6666
var buffer = new StringBuffer();
6767
if (asConst) {
6868
buffer.write('const ');

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class JsonSerializableGenerator
6363
/// proper deserialization of types.
6464
///
6565
/// If an exception is thrown during deserialization, a
66-
/// [SerializationConvertException] is thrown.
66+
/// [CheckedFromJsonException] is thrown.
6767
final bool checked;
6868

6969
/// Creates an instance of [JsonSerializableGenerator].

json_serializable/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ dev_dependencies:
2424
logging: ^0.11.3+1
2525
test: ^0.12.3
2626
yaml: ^2.1.13
27+
28+
dependency_overrides:
29+
json_annotation:
30+
path: ../json_annotation

json_serializable/test/kitchen_sink_test.dart

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
import 'package:test/test.dart';
66

7+
import 'package:json_annotation/json_annotation.dart';
78
import 'package:json_serializable/src/constants.dart';
89
import 'package:yaml/yaml.dart';
910

1011
import 'test_files/kitchen_sink.dart' as nullable
1112
show testFactory, testFromJson;
13+
import 'test_files/kitchen_sink.non_nullable.checked.dart' as checked
14+
show testFactory, testFromJson;
1215
import 'test_files/kitchen_sink.non_nullable.dart' as nn
1316
show testFactory, testFromJson;
1417
import 'test_files/kitchen_sink.non_nullable.wrapped.dart' as nnwrapped
@@ -38,6 +41,11 @@ void main() {
3841
});
3942

4043
group('non-nullable', () {
44+
group('checked', () {
45+
_nonNullableTests(checked.testFactory, checked.testFromJson,
46+
isChecked: true);
47+
});
48+
4149
group('unwrapped', () {
4250
_nonNullableTests(nn.testFactory, nn.testFromJson);
4351
});
@@ -56,16 +64,21 @@ typedef KitchenSink KitchenSinkCtor(
5664
Iterable<int> intIterable,
5765
Iterable<DateTime> dateTimeIterable});
5866

59-
void _nonNullableTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json)) {
67+
void _nonNullableTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json),
68+
{bool isChecked: false}) {
6069
test('with null values fails serialization', () {
6170
expect(() => (ctor()..objectDateTimeMap = null).toJson(),
6271
throwsNoSuchMethodError);
6372
});
6473

6574
test('with empty json fails deserialization', () {
66-
expect(() => fromJson({}), throwsNoSuchMethodError);
75+
if (isChecked) {
76+
expect(() => fromJson({}), _checkedMatcher(true, 'intIterable'));
77+
} else {
78+
expect(() => fromJson({}), throwsNoSuchMethodError);
79+
}
6780
});
68-
_sharedTests(ctor, fromJson);
81+
_sharedTests(ctor, fromJson, isChecked: isChecked);
6982
}
7083

7184
void _nullableTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json)) {
@@ -122,7 +135,8 @@ void _nullableTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json)) {
122135
_sharedTests(ctor, fromJson);
123136
}
124137

125-
void _sharedTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json)) {
138+
void _sharedTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json),
139+
{bool isChecked: false}) {
126140
void roundTripSink(KitchenSink p) {
127141
roundTripObject(p, fromJson);
128142
}
@@ -183,24 +197,38 @@ void _sharedTests(KitchenSinkCtor ctor, KitchenSink fromJson(Map json)) {
183197
});
184198

185199
group('a bad value for', () {
186-
for (var e in _invalidValues.entries) {
200+
for (var invalidEntry in _invalidValues.entries) {
201+
final expectedKeyValue =
202+
const ['intIterable', 'datetime-iterable'].contains(invalidEntry.key)
203+
? null
204+
: invalidEntry.key;
205+
final matcher = _checkedMatcher(isChecked, expectedKeyValue);
206+
187207
for (var isJson in [true, false]) {
188-
test('`${e.key}` fails - ${isJson ? 'json' : 'yaml'}', () {
208+
test('`${invalidEntry.key}` fails - ${isJson ? 'json' : 'yaml'}', () {
189209
var copy = new Map.from(_validValues);
190-
copy[e.key] = e.value;
210+
copy[invalidEntry.key] = invalidEntry.value;
191211

192212
if (!isJson) {
193213
copy = loadYaml(loudEncode(copy)) as YamlMap;
194214
}
195215

196-
expect(() => fromJson(copy),
197-
throwsA(anyOf(_isACastError, _isATypeError, isArgumentError)));
216+
expect(() => fromJson(copy), matcher);
198217
});
199218
}
200219
}
201220
});
202221
}
203222

223+
Matcher _checkedMatcher(bool checked, String expectedKey) => throwsA(checked
224+
? allOf(
225+
const isInstanceOf<CheckedFromJsonException>(),
226+
new FeatureMatcher<CheckedFromJsonException>(
227+
'className', (e) => e.className, 'KitchenSink'),
228+
new FeatureMatcher<CheckedFromJsonException>(
229+
'key', (e) => e.key, expectedKey))
230+
: anyOf(_isACastError, _isATypeError, isArgumentError));
231+
204232
final _validValues = const {
205233
'no-42': 0,
206234
'dateTime': '2018-05-10T14:20:58.927',

json_serializable/test/test_files/kitchen_sink.non_nullable.checked.g.dart

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,74 @@ part of 'kitchen_sink.non_nullable.checked.dart';
1010
// Generator: JsonSerializableGenerator
1111
// **************************************************************************
1212

13-
KitchenSink _$KitchenSinkFromJson(Map json) => new KitchenSink(
14-
ctorValidatedNo42: json['no-42'] as int,
15-
iterable: json['iterable'] as List,
16-
dynamicIterable: json['dynamicIterable'] as List,
17-
objectIterable: json['objectIterable'] as List,
18-
intIterable: (json['intIterable'] as List).map((e) => e as int),
19-
dateTimeIterable: (json['datetime-iterable'] as List)
20-
.map((e) => DateTime.parse(e as String)))
21-
..dateTime = DateTime.parse(json['dateTime'] as String)
22-
..list = json['list'] as List
23-
..dynamicList = json['dynamicList'] as List
24-
..objectList = json['objectList'] as List
25-
..intList = (json['intList'] as List).map((e) => e as int).toList()
26-
..dateTimeList = (json['dateTimeList'] as List)
27-
.map((e) => DateTime.parse(e as String))
28-
.toList()
29-
..map = json['map'] as Map
30-
..stringStringMap =
31-
new Map<String, String>.from(json['stringStringMap'] as Map)
32-
..dynamicIntMap = new Map<String, int>.from(json['dynamicIntMap'] as Map)
33-
..objectDateTimeMap = (json['objectDateTimeMap'] as Map)
34-
.map((k, e) => new MapEntry(k, DateTime.parse(e as String)))
35-
..crazyComplex = (json['crazyComplex'] as List)
36-
.map((e) => (e as Map).map((k, e) => new MapEntry(
37-
k as String,
38-
(e as Map).map(
39-
(k, e) => new MapEntry(k as String, (e as List).map((e) => (e as List).map((e) => DateTime.parse(e as String)).toList()).toList())))))
40-
.toList()
41-
..val = new Map<String, bool>.from(json['val'] as Map)
42-
..writeNotNull = json['writeNotNull'] as bool
43-
..string = json[r'$string'] as String
44-
..simpleObject = new SimpleObject.fromJson(json['simpleObject'] as Map);
13+
KitchenSink _$KitchenSinkFromJson(Map json) => $checkedNew(
14+
'KitchenSink',
15+
json,
16+
const {
17+
'ctorValidatedNo42': 'no-42',
18+
'iterable': 'iterable',
19+
'dynamicIterable': 'dynamicIterable',
20+
'objectIterable': 'objectIterable',
21+
'intIterable': 'intIterable',
22+
'dateTimeIterable': 'datetime-iterable',
23+
'dateTime': 'dateTime',
24+
'list': 'list',
25+
'dynamicList': 'dynamicList',
26+
'objectList': 'objectList',
27+
'intList': 'intList',
28+
'dateTimeList': 'dateTimeList',
29+
'map': 'map',
30+
'stringStringMap': 'stringStringMap',
31+
'dynamicIntMap': 'dynamicIntMap',
32+
'objectDateTimeMap': 'objectDateTimeMap',
33+
'crazyComplex': 'crazyComplex',
34+
'val': 'val',
35+
'writeNotNull': 'writeNotNull',
36+
'string': r'$string',
37+
'simpleObject': 'simpleObject'
38+
},
39+
() => new KitchenSink(
40+
ctorValidatedNo42:
41+
$checkedConvert(json, 'no-42', () => json['no-42'] as int),
42+
iterable:
43+
$checkedConvert(json, 'iterable', () => json['iterable'] as List),
44+
dynamicIterable: $checkedConvert(
45+
json, 'dynamicIterable', () => json['dynamicIterable'] as List),
46+
objectIterable: $checkedConvert(
47+
json, 'objectIterable', () => json['objectIterable'] as List),
48+
intIterable: $checkedConvert(json, 'intIterable',
49+
() => (json['intIterable'] as List).map((e) => e as int)),
50+
dateTimeIterable: $checkedConvert(
51+
json,
52+
'datetime-iterable',
53+
() => (json['datetime-iterable'] as List)
54+
.map((e) => DateTime.parse(e as String))))
55+
..dateTime = $checkedConvert(
56+
json, 'dateTime', () => DateTime.parse(json['dateTime'] as String))
57+
..list = $checkedConvert(json, 'list', () => json['list'] as List)
58+
..dynamicList = $checkedConvert(
59+
json, 'dynamicList', () => json['dynamicList'] as List)
60+
..objectList =
61+
$checkedConvert(json, 'objectList', () => json['objectList'] as List)
62+
..intList = $checkedConvert(json, 'intList',
63+
() => (json['intList'] as List).map((e) => e as int).toList())
64+
..dateTimeList = $checkedConvert(
65+
json,
66+
'dateTimeList',
67+
() => (json['dateTimeList'] as List)
68+
.map((e) => DateTime.parse(e as String))
69+
.toList())
70+
..map = $checkedConvert(json, 'map', () => json['map'] as Map)
71+
..stringStringMap = $checkedConvert(json, 'stringStringMap',
72+
() => new Map<String, String>.from(json['stringStringMap'] as Map))
73+
..dynamicIntMap =
74+
$checkedConvert(json, 'dynamicIntMap', () => new Map<String, int>.from(json['dynamicIntMap'] as Map))
75+
..objectDateTimeMap = $checkedConvert(json, 'objectDateTimeMap', () => (json['objectDateTimeMap'] as Map).map((k, e) => new MapEntry(k, DateTime.parse(e as String))))
76+
..crazyComplex = $checkedConvert(json, 'crazyComplex', () => (json['crazyComplex'] as List).map((e) => (e as Map).map((k, e) => new MapEntry(k as String, (e as Map).map((k, e) => new MapEntry(k as String, (e as List).map((e) => (e as List).map((e) => DateTime.parse(e as String)).toList()).toList()))))).toList())
77+
..val = $checkedConvert(json, 'val', () => new Map<String, bool>.from(json['val'] as Map))
78+
..writeNotNull = $checkedConvert(json, 'writeNotNull', () => json['writeNotNull'] as bool)
79+
..string = $checkedConvert(json, r'$string', () => json[r'$string'] as String)
80+
..simpleObject = $checkedConvert(json, 'simpleObject', () => new SimpleObject.fromJson(json['simpleObject'] as Map)));
4581

4682
abstract class _$KitchenSinkSerializerMixin {
4783
int get ctorValidatedNo42;

json_serializable/tool/build.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ final List<BuilderApplication> builders = [
4343
include: const [
4444
'test/test_files/kitchen_sink.dart',
4545
'test/test_files/kitchen_sink.non_nullable.dart',
46-
'test/test_files/kitchen_sink.non_nullable.checked.dart',
4746
'test/test_files/simple_object.dart'
4847
],
4948
)),
49+
applyToRoot(
50+
jsonPartBuilder(header: copyrightHeader, checked: true, anyMap: true),
51+
generateFor: const InputSet(
52+
include: const [
53+
'test/config/build_config.dart',
54+
'test/test_files/kitchen_sink.non_nullable.checked.dart'
55+
],
56+
)),
5057
applyToRoot(jsonPartBuilder(useWrappers: true, header: copyrightHeader),
5158
generateFor: const InputSet(
5259
include: const [

0 commit comments

Comments
 (0)