Skip to content

Commit fd874dd

Browse files
committed
Add option for explicitToJson
Allows users to opt-in to including `.toJson()` calls on nested objects in generated `toJson` methods. This is useful if you want to round-trip the output of toJson without putting it through jsonEncode and jsonDecode Fixes #192
1 parent ddc4b02 commit fd874dd

10 files changed

+160
-14
lines changed

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* Added missing `checked` parameter to the
1818
`JsonSerializableGenerator.withDefaultHelpers` constructor.
1919

20+
* Added `explicit_to_json` configuration option.
21+
2022
## 0.5.6
2123

2224
* Added support for `JsonSerializable.disallowUnrecognizedKeys`.

json_serializable/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ targets:
9292
use_wrappers: true
9393
any_map: true
9494
checked: true
95+
explicit_to_json: true
9596
```
9697
9798
[example]: https://github.com/dart-lang/json_serializable/blob/master/example

json_serializable/lib/builder.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ Builder jsonSerializable(BuilderOptions options) {
2626
var optionsMap = new Map<String, dynamic>.from(options.config);
2727

2828
var builder = jsonPartBuilder(
29-
header: optionsMap.remove('header') as String,
30-
useWrappers: optionsMap.remove('use_wrappers') as bool,
31-
checked: optionsMap.remove('checked') as bool,
32-
anyMap: optionsMap.remove('any_map') as bool);
29+
header: optionsMap.remove('header') as String,
30+
useWrappers: optionsMap.remove('use_wrappers') as bool,
31+
checked: optionsMap.remove('checked') as bool,
32+
anyMap: optionsMap.remove('any_map') as bool,
33+
explicitToJson: optionsMap.remove('explicit_to_json') as bool,
34+
);
3335

3436
if (optionsMap.isNotEmpty) {
3537
if (log == null) {

json_serializable/lib/src/json_part_builder.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,21 @@ import 'json_serializable_generator.dart';
2020
///
2121
/// For details on [useWrappers], [anyMap], and [checked] see
2222
/// [JsonSerializableGenerator].
23-
Builder jsonPartBuilder(
24-
{String header,
25-
String formatOutput(String code),
26-
bool useWrappers: false,
27-
bool anyMap: false,
28-
bool checked: false}) {
23+
Builder jsonPartBuilder({
24+
String header,
25+
String formatOutput(String code),
26+
bool useWrappers: false,
27+
bool anyMap: false,
28+
bool checked: false,
29+
bool explicitToJson: false,
30+
}) {
2931
return new PartBuilder([
3032
new JsonSerializableGenerator(
31-
useWrappers: useWrappers, anyMap: anyMap, checked: checked),
33+
useWrappers: useWrappers,
34+
anyMap: anyMap,
35+
checked: checked,
36+
explicitToJson: explicitToJson,
37+
),
3238
const JsonLiteralGenerator()
3339
], header: header, formatOutput: formatOutput);
3440
}

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ class JsonSerializableGenerator
6868
/// [CheckedFromJsonException] is thrown.
6969
final bool checked;
7070

71+
/// If `true`, generated `toJson` methods will explicitly call `toJson` on
72+
/// nested objects.
73+
///
74+
/// When using JSON encoding support in `dart:convert`, `toJson` is
75+
/// automatically called on objects, so the default behavior
76+
/// (`explicitToJson: false`) is to omit the `toJson` call.
77+
///
78+
/// Example of `explicitToJson: false` (default)
79+
///
80+
/// ```dart
81+
/// Map<String, dynamic> toJson() => {'child': child};
82+
/// ```
83+
///
84+
/// Example of `explicitToJson: true`
85+
///
86+
/// ```dart
87+
/// Map<String, dynamic> toJson() => {'child': child?.toJson()};
88+
/// ```
89+
final bool explicitToJson;
90+
7191
/// Creates an instance of [JsonSerializableGenerator].
7292
///
7393
/// If [typeHelpers] is not provided, three built-in helpers are used:
@@ -77,9 +97,11 @@ class JsonSerializableGenerator
7797
bool useWrappers: false,
7898
bool anyMap: false,
7999
bool checked: false,
100+
bool explicitToJson: false,
80101
}) : this.useWrappers = useWrappers ?? false,
81102
this.anyMap = anyMap ?? false,
82103
this.checked = checked ?? false,
104+
this.explicitToJson = explicitToJson ?? false,
83105
this._typeHelpers = typeHelpers ?? _defaultHelpers;
84106

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

json_serializable/lib/src/type_helper_context.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {
2121

2222
bool get anyMap => _generator.anyMap;
2323

24+
bool get explicitToJson => _generator.explicitToJson;
25+
2426
@override
2527
bool get nullable => _key.nullable;
2628

json_serializable/lib/src/type_helpers/json_helper.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ class JsonHelper extends TypeHelper {
2121
/// By default, JSON encoding in from `dart:convert` calls `toJson()` on
2222
/// provided objects.
2323
@override
24-
String serialize(DartType targetType, String expression, _) {
24+
String serialize(
25+
DartType targetType, String expression, SerializeContext context) {
2526
if (!_canSerialize(targetType)) {
2627
return null;
2728
}
2829

30+
if (context is TypeHelperContext && context.explicitToJson) {
31+
return '$expression${context.nullable ? '?' : ''}.toJson()';
32+
}
2933
return expression;
3034
}
3135

json_serializable/test/config_test.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,14 @@ const _validConfig = const {
100100
'header': 'header',
101101
'use_wrappers': true,
102102
'any_map': true,
103-
'checked': true
103+
'checked': true,
104+
'explicit_to_json': true
104105
};
105106

106107
const _invalidConfig = const {
107108
'header': true,
108109
'use_wrappers': 42,
109110
'any_map': 42,
110-
'checked': 42
111+
'checked': 42,
112+
'explicit_to_json': 42
111113
};

json_serializable/test/json_serializable_test.dart

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,99 @@ void _registerTests(JsonSerializableGenerator generator) {
6666
_throwsInvalidGenerationSourceError(messageMatcher, todoMatcher));
6767
}
6868

69+
group('explicit toJson', () {
70+
test('nullable', () async {
71+
var output = await _runForElementNamed(
72+
new JsonSerializableGenerator(
73+
explicitToJson: true, useWrappers: generator.useWrappers),
74+
'TrivialNestedNullable');
75+
76+
var expected = generator.useWrappers
77+
? r'''abstract class _$TrivialNestedNullableSerializerMixin {
78+
TrivialNestedNullable get child;
79+
int get otherField;
80+
Map<String, dynamic> toJson() =>
81+
new _$TrivialNestedNullableJsonMapWrapper(this);
82+
}
83+
84+
class _$TrivialNestedNullableJsonMapWrapper extends $JsonMapWrapper {
85+
final _$TrivialNestedNullableSerializerMixin _v;
86+
_$TrivialNestedNullableJsonMapWrapper(this._v);
87+
88+
@override
89+
Iterable<String> get keys => const ['child', 'otherField'];
90+
91+
@override
92+
dynamic operator [](Object key) {
93+
if (key is String) {
94+
switch (key) {
95+
case 'child':
96+
return _v.child?.toJson();
97+
case 'otherField':
98+
return _v.otherField;
99+
}
100+
}
101+
return null;
102+
}
103+
}
104+
'''
105+
: r'''abstract class _$TrivialNestedNullableSerializerMixin {
106+
TrivialNestedNullable get child;
107+
int get otherField;
108+
Map<String, dynamic> toJson() =>
109+
<String, dynamic>{'child': child?.toJson(), 'otherField': otherField};
110+
}
111+
''';
112+
113+
expect(output, expected);
114+
});
115+
test('non-nullable', () async {
116+
var output = await _runForElementNamed(
117+
new JsonSerializableGenerator(
118+
explicitToJson: true, useWrappers: generator.useWrappers),
119+
'TrivialNestedNonNullable');
120+
121+
var expected = generator.useWrappers
122+
? r'''abstract class _$TrivialNestedNonNullableSerializerMixin {
123+
TrivialNestedNonNullable get child;
124+
int get otherField;
125+
Map<String, dynamic> toJson() =>
126+
new _$TrivialNestedNonNullableJsonMapWrapper(this);
127+
}
128+
129+
class _$TrivialNestedNonNullableJsonMapWrapper extends $JsonMapWrapper {
130+
final _$TrivialNestedNonNullableSerializerMixin _v;
131+
_$TrivialNestedNonNullableJsonMapWrapper(this._v);
132+
133+
@override
134+
Iterable<String> get keys => const ['child', 'otherField'];
135+
136+
@override
137+
dynamic operator [](Object key) {
138+
if (key is String) {
139+
switch (key) {
140+
case 'child':
141+
return _v.child.toJson();
142+
case 'otherField':
143+
return _v.otherField;
144+
}
145+
}
146+
return null;
147+
}
148+
}
149+
'''
150+
: r'''abstract class _$TrivialNestedNonNullableSerializerMixin {
151+
TrivialNestedNonNullable get child;
152+
int get otherField;
153+
Map<String, dynamic> toJson() =>
154+
<String, dynamic>{'child': child.toJson(), 'otherField': otherField};
155+
}
156+
''';
157+
158+
expect(output, expected);
159+
});
160+
});
161+
69162
group('non-classes', () {
70163
test('const field', () {
71164
expectThrows('theAnswer', 'Generator cannot target `theAnswer`.',

json_serializable/test/src/json_serializable_test_input.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,15 @@ class SuperType {
220220
int priceFraction(int other) =>
221221
superTypeViaCtor == null ? null : superTypeViaCtor ~/ other;
222222
}
223+
224+
@JsonSerializable(createFactory: false)
225+
class TrivialNestedNullable {
226+
TrivialNestedNullable child;
227+
int otherField;
228+
}
229+
230+
@JsonSerializable(createFactory: false, nullable: false)
231+
class TrivialNestedNonNullable {
232+
TrivialNestedNonNullable child;
233+
int otherField;
234+
}

0 commit comments

Comments
 (0)