Skip to content

Commit f9c2c8e

Browse files
committed
Support the nullable field on the JsonSerializable class annotation
json_annotation: fix doc comments
1 parent 50d3e97 commit f9c2c8e

15 files changed

+443
-289
lines changed

json_annotation/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.2.1
2+
3+
* `JsonSerializable` class annotation
4+
* Added `nullable` field.
5+
* Fixed doc comment.
6+
17
## 0.2.0
28

3-
- Moved annotation classes for `JsonSerializable` and `JsonLiteral`.
9+
* Moved annotation classes for `JsonSerializable` and `JsonLiteral`.

json_annotation/lib/src/json_serializable.dart

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,34 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
class JsonSerializable {
6+
// TODO(kevmoo): document these fields
67
final bool createFactory;
78
final bool createToJson;
89

9-
/// Whether the generator should include the this field in the serialized
10-
/// output, even if the value is `null`.
10+
/// Whether the generator should include fields with `null` values in the
11+
/// serialized output.
1112
final bool includeIfNull;
1213

14+
/// When `true` (the default), `null` values are handled gracefully when
15+
/// serializing fields to JSON and when deserializing `null` and nonexistent
16+
/// values from a JSON map.
17+
///
18+
/// Setting to `false` eliminates `null` verification in the generated code,
19+
/// which reduces the code size. Errors may be thrown at runtime if `null`
20+
/// values are encountered, but the original class should also implement
21+
/// `null` runtime validation if it's critical.
22+
final bool nullable;
23+
24+
// TODO(kevmoo): document the constructor
1325
const JsonSerializable(
1426
{bool createFactory: true,
1527
bool createToJson: true,
16-
bool includeIfNull: true})
28+
bool includeIfNull: true,
29+
bool nullable: true})
1730
: this.createFactory = createFactory ?? true,
1831
this.createToJson = createToJson ?? true,
19-
this.includeIfNull = includeIfNull ?? true;
32+
this.includeIfNull = includeIfNull ?? true,
33+
this.nullable = nullable ?? true;
2034
}
2135

2236
/// An annotation used to specify how a field is serialized.
@@ -27,15 +41,21 @@ class JsonKey {
2741
/// If `null`, the field name is used.
2842
final String name;
2943

30-
/// [true] if the generator should validate all values for `null` in
31-
/// serialization code.
44+
/// When `true`, `null` values are handled gracefully when
45+
/// serializing the field to JSON and when deserializing `null` and
46+
/// nonexistent values from a JSON map.
3247
///
33-
/// Setting to [false] eliminates `null` verification in generated code, but
34-
/// does not prevent `null` values from being created. Annotated classes
35-
/// must implement their own `null` validation.
48+
/// Setting to `false` eliminates `null` verification in the generated code
49+
/// for the annotated field, which reduces the code size. Errors may be thrown
50+
/// at runtime if `null` values are encountered, but the original class should
51+
/// also implement `null` runtime validation if it's critical.
52+
///
53+
/// The default value, `null`, indicates that the behavior should be
54+
/// acquired from the [JsonSerializable.nullable] annotation on the
55+
/// enclosing class.
3656
final bool nullable;
3757

38-
/// [true] if the generator should include the this field in the serialized
58+
/// `true` if the generator should include the this field in the serialized
3959
/// output, even if the value is `null`.
4060
///
4161
/// The default value, `null`, indicates that the behavior should be
@@ -46,7 +66,5 @@ class JsonKey {
4666
/// Creates a new [JsonKey].
4767
///
4868
/// Only required when the default behavior is not desired.
49-
const JsonKey({this.name, bool nullable: true, bool includeIfNull})
50-
: this.nullable = nullable ?? true,
51-
this.includeIfNull = includeIfNull;
69+
const JsonKey({this.name, this.nullable, this.includeIfNull});
5270
}

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.0
2+
version: 0.2.1-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Throw an exception if a duplicate JSON key is detected.
44

5+
* Support the `nullable` field on the `JsonSerializable` class annotation.
6+
57
## 0.2.4+1
68

79
* Throw a more helpful error when a constructor is missing.

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,11 @@ class JsonSerializableGenerator
106106

107107
var buffer = new StringBuffer();
108108

109-
if (annotation.read('createFactory').boolValue) {
110-
var toSkip = _writeFactory(buffer, classElement, fields, prefix);
109+
final classAnnotation = _valueForAnnotation(annotation);
110+
111+
if (classAnnotation.createFactory) {
112+
var toSkip = _writeFactory(
113+
buffer, classElement, fields, prefix, classAnnotation.nullable);
111114

112115
// If there are fields that are final – that are not set via the generated
113116
// constructor, then don't output them when generating the `toJson` call.
@@ -130,7 +133,7 @@ class JsonSerializableGenerator
130133
return set;
131134
});
132135

133-
if (annotation.read('createToJson').boolValue) {
136+
if (classAnnotation.createToJson) {
134137
//
135138
// Generate the mixin class
136139
//
@@ -143,15 +146,14 @@ class JsonSerializableGenerator
143146
buffer.writeln(' ${field.type} get ${field.name};');
144147
}
145148

146-
var includeIfNull = annotation.read('includeIfNull').boolValue;
147-
148149
buffer.writeln(' Map<String, dynamic> toJson() ');
149-
if (fieldsList.every((e) => _includeIfNull(e, includeIfNull))) {
150+
if (fieldsList
151+
.every((e) => _includeIfNull(e, classAnnotation.includeIfNull))) {
150152
// write simple `toJson` method that includes all keys...
151-
_writeToJsonSimple(buffer, fields.values);
153+
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
152154
} else {
153155
// At least one field should be excluded if null
154-
_writeToJsonWithNullChecks(buffer, fields.values, includeIfNull);
156+
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
155157
}
156158

157159
// end of the mixin class
@@ -162,7 +164,7 @@ class JsonSerializableGenerator
162164
}
163165

164166
void _writeToJsonWithNullChecks(StringBuffer buffer,
165-
Iterable<FieldElement> fields, bool classIncludeIfNull) {
167+
Iterable<FieldElement> fields, JsonSerializable classAnnotation) {
166168
buffer.writeln('{');
167169

168170
buffer.writeln('var $toJsonMapVarName = <String, dynamic>{');
@@ -184,13 +186,14 @@ class JsonSerializableGenerator
184186
safeFieldAccess = 'this.$safeFieldAccess';
185187
}
186188

187-
if (_includeIfNull(field, classIncludeIfNull)) {
189+
var expression = _serializeField(field, classAnnotation.nullable,
190+
accessOverride: safeFieldAccess);
191+
if (_includeIfNull(field, classAnnotation.includeIfNull)) {
188192
if (directWrite) {
189-
buffer.writeln('$safeJsonKeyString : '
190-
'${_serializeField(field, accessOverride: safeFieldAccess)},');
193+
buffer.writeln('$safeJsonKeyString : $expression,');
191194
} else {
192-
buffer.writeln('$toJsonMapVarName[$safeJsonKeyString] = '
193-
'${_serializeField(field, accessOverride: safeFieldAccess)};');
195+
buffer
196+
.writeln('$toJsonMapVarName[$safeJsonKeyString] = $expression;');
194197
}
195198
} else {
196199
if (directWrite) {
@@ -208,8 +211,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
208211
}''');
209212
directWrite = false;
210213
}
211-
buffer.writeln('$toJsonMapHelperName($safeJsonKeyString, '
212-
'${_serializeField(field, accessOverride: safeFieldAccess)});');
214+
buffer
215+
.writeln('$toJsonMapHelperName($safeJsonKeyString, $expression);');
213216
}
214217
}
215218

@@ -218,12 +221,14 @@ void $toJsonMapHelperName(String key, dynamic value) {
218221
buffer.writeln('}');
219222
}
220223

221-
void _writeToJsonSimple(StringBuffer buffer, Iterable<FieldElement> fields) {
224+
void _writeToJsonSimple(StringBuffer buffer, Iterable<FieldElement> fields,
225+
bool classSupportNullable) {
222226
buffer.writeln('=> <String, dynamic>{');
223227

224228
var pairs = <String>[];
225229
for (var field in fields) {
226-
pairs.add('${_safeNameAccess(field)}: ${_serializeField(field )}');
230+
pairs.add(
231+
'${_safeNameAccess(field)}: ${_serializeField(field, classSupportNullable )}');
227232
}
228233
buffer.writeAll(pairs, ',\n');
229234

@@ -235,7 +240,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
235240
StringBuffer buffer,
236241
ClassElement classElement,
237242
Map<String, FieldElement> fields,
238-
String prefix) {
243+
String prefix,
244+
bool classSupportNullable) {
239245
// creating a copy so it can be mutated
240246
var fieldsToSet = new Map<String, FieldElement>.from(fields);
241247
var className = classElement.displayName;
@@ -303,7 +309,7 @@ void $toJsonMapHelperName(String key, dynamic value) {
303309
buffer.write(' new $className(');
304310
buffer.writeAll(
305311
ctorArguments.map((paramElement) => _deserializeForField(
306-
fields[paramElement.name],
312+
fields[paramElement.name], classSupportNullable,
307313
ctorParam: paramElement)),
308314
', ');
309315
if (ctorArguments.isNotEmpty && ctorNamedArguments.isNotEmpty) {
@@ -312,7 +318,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
312318
buffer.writeAll(
313319
ctorNamedArguments.map((paramElement) =>
314320
'${paramElement.name}: ' +
315-
_deserializeForField(fields[paramElement.name],
321+
_deserializeForField(
322+
fields[paramElement.name], classSupportNullable,
316323
ctorParam: paramElement)),
317324
', ');
318325

@@ -323,7 +330,7 @@ void $toJsonMapHelperName(String key, dynamic value) {
323330
for (var field in fieldsToSet.values) {
324331
buffer.writeln();
325332
buffer.write(' ..${field.name} = ');
326-
buffer.write(_deserializeForField(field));
333+
buffer.write(_deserializeForField(field, classSupportNullable));
327334
}
328335
buffer.writeln(';');
329336
}
@@ -335,10 +342,12 @@ void $toJsonMapHelperName(String key, dynamic value) {
335342
Iterable<TypeHelper> get _allHelpers =>
336343
[_typeHelpers, _coreHelpers].expand((e) => e);
337344

338-
String _serializeField(FieldElement field, {String accessOverride}) {
345+
String _serializeField(FieldElement field, bool classIncludeNullable,
346+
{String accessOverride}) {
339347
accessOverride ??= field.name;
340348
try {
341-
return _serialize(field.type, accessOverride, _nullable(field));
349+
return _serialize(
350+
field.type, accessOverride, _nullable(field, classIncludeNullable));
342351
} on UnsupportedTypeError {
343352
throw new InvalidGenerationSourceError(
344353
'Could not generate `toJson` code for '
@@ -356,14 +365,15 @@ void $toJsonMapHelperName(String key, dynamic value) {
356365
orElse: () =>
357366
throw new UnsupportedTypeError(targetType, expression));
358367

359-
String _deserializeForField(FieldElement field,
368+
String _deserializeForField(FieldElement field, bool classSupportNullable,
360369
{ParameterElement ctorParam}) {
361370
var jsonKey = _safeNameAccess(field);
362371

363372
var targetType = ctorParam?.type ?? field.type;
364373

365374
try {
366-
return _deserialize(targetType, 'json[$jsonKey]', _nullable(field));
375+
return _deserialize(
376+
targetType, 'json[$jsonKey]', _nullable(field, classSupportNullable));
367377
} on UnsupportedTypeError {
368378
throw new InvalidGenerationSourceError(
369379
'Could not generate fromJson code for '
@@ -390,10 +400,11 @@ String _safeNameAccess(FieldElement field) {
390400
/// Returns `true` if the field should be treated as potentially nullable.
391401
///
392402
/// If no [JsonKey] annotation is present on the field, `true` is returned.
393-
bool _nullable(FieldElement field) => _jsonKeyFor(field).nullable;
403+
bool _nullable(FieldElement field, bool parentValue) =>
404+
_jsonKeyFor(field).nullable ?? parentValue;
394405

395-
bool _includeIfNull(FieldElement element, bool parentValue) =>
396-
_jsonKeyFor(element).includeIfNull ?? parentValue;
406+
bool _includeIfNull(FieldElement field, bool parentValue) =>
407+
_jsonKeyFor(field).includeIfNull ?? parentValue;
397408

398409
JsonKey _jsonKeyFor(FieldElement element) {
399410
var key = _jsonKeyExpando[element];
@@ -416,6 +427,13 @@ JsonKey _jsonKeyFor(FieldElement element) {
416427
return key;
417428
}
418429

430+
JsonSerializable _valueForAnnotation(ConstantReader annotation) =>
431+
new JsonSerializable(
432+
createToJson: annotation.read('createToJson').boolValue,
433+
createFactory: annotation.read('createFactory').boolValue,
434+
nullable: annotation.read('nullable').boolValue,
435+
includeIfNull: annotation.read('includeIfNull').boolValue);
436+
419437
final _jsonKeyExpando = new Expando<JsonKey>();
420438

421439
final _jsonKeyChecker = new TypeChecker.fromRuntime(JsonKey);

json_serializable/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
@TestOn('!browser')
66
library json_serializable.test.json_generator_test;
77

8+
// TODO(kevmoo): test all flavors of `nullable` - class, fields, etc
9+
810
import 'dart:async';
911

1012
import 'package:analyzer/dart/ast/ast.dart';

json_serializable/test/kitchen_sink_test.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:test/test.dart';
66

77
import 'package:json_serializable/src/utils.dart';
88

9-
import 'test_files/bathtub.dart';
109
import 'test_files/kitchen_sink.dart';
10+
import 'test_files/kitchen_sink.non_nullable.dart';
1111
import 'test_utils.dart';
1212

1313
void main() {
@@ -80,12 +80,15 @@ void main() {
8080

8181
group('BathTub', () {
8282
test('with null values fails serialization', () {
83-
expect(() => (new Bathtub()..stringDateTimeMap = null).toJson(),
83+
expect(
84+
() =>
85+
(new KitchenSinkNonNullable()..stringDateTimeMap = null).toJson(),
8486
throwsNoSuchMethodError);
8587
});
8688

8789
test('with empty json fails deserialization', () {
88-
expect(() => new Bathtub.fromJson({}), throwsNoSuchMethodError);
90+
expect(() => new KitchenSinkNonNullable.fromJson({}),
91+
throwsNoSuchMethodError);
8992
});
9093

9194
_sharedTests(
@@ -95,13 +98,13 @@ void main() {
9598
Iterable<Object> objectIterable,
9699
Iterable<int> intIterable,
97100
Iterable<DateTime> dateTimeIterable}) =>
98-
new Bathtub(
101+
new KitchenSinkNonNullable(
99102
iterable: iterable,
100103
dynamicIterable: dynamicIterable,
101104
objectIterable: objectIterable,
102105
intIterable: intIterable,
103106
dateTimeIterable: dateTimeIterable),
104-
(j) => new Bathtub.fromJson(j));
107+
(j) => new KitchenSinkNonNullable.fromJson(j));
105108
});
106109
}
107110

0 commit comments

Comments
 (0)