Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* Added missing `checked` parameter to the
`JsonSerializableGenerator.withDefaultHelpers` constructor.

* Added `explicit_to_json` configuration option.

## 0.5.6

* Added support for `JsonSerializable.disallowUnrecognizedKeys`.
Expand Down
1 change: 1 addition & 0 deletions json_serializable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ targets:
use_wrappers: true
any_map: true
checked: true
explicit_to_json: true
```

[example]: https://github.com/dart-lang/json_serializable/blob/master/example
Expand Down
10 changes: 6 additions & 4 deletions json_serializable/lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ Builder jsonSerializable(BuilderOptions options) {
var optionsMap = new Map<String, dynamic>.from(options.config);

var builder = jsonPartBuilder(
header: optionsMap.remove('header') as String,
useWrappers: optionsMap.remove('use_wrappers') as bool,
checked: optionsMap.remove('checked') as bool,
anyMap: optionsMap.remove('any_map') as bool);
header: optionsMap.remove('header') as String,
useWrappers: optionsMap.remove('use_wrappers') as bool,
checked: optionsMap.remove('checked') as bool,
anyMap: optionsMap.remove('any_map') as bool,
explicitToJson: optionsMap.remove('explicit_to_json') as bool,
);

if (optionsMap.isNotEmpty) {
if (log == null) {
Expand Down
20 changes: 13 additions & 7 deletions json_serializable/lib/src/json_part_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,21 @@ import 'json_serializable_generator.dart';
///
/// For details on [useWrappers], [anyMap], and [checked] see
/// [JsonSerializableGenerator].
Builder jsonPartBuilder(
{String header,
String formatOutput(String code),
bool useWrappers: false,
bool anyMap: false,
bool checked: false}) {
Builder jsonPartBuilder({
String header,
String formatOutput(String code),
bool useWrappers: false,
bool anyMap: false,
bool checked: false,
bool explicitToJson: false,
}) {
return new PartBuilder([
new JsonSerializableGenerator(
useWrappers: useWrappers, anyMap: anyMap, checked: checked),
useWrappers: useWrappers,
anyMap: anyMap,
checked: checked,
explicitToJson: explicitToJson,
),
const JsonLiteralGenerator()
], header: header, formatOutput: formatOutput);
}
22 changes: 22 additions & 0 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ class JsonSerializableGenerator
/// [CheckedFromJsonException] is thrown.
final bool checked;

/// If `true`, generated `toJson` methods will explicitly call `toJson` on
/// nested objects.
///
/// When using JSON encoding support in `dart:convert`, `toJson` is
/// automatically called on objects, so the default behavior
/// (`explicitToJson: false`) is to omit the `toJson` call.
///
/// Example of `explicitToJson: false` (default)
///
/// ```dart
/// Map<String, dynamic> toJson() => {'child': child};
/// ```
///
/// Example of `explicitToJson: true`
///
/// ```dart
/// Map<String, dynamic> toJson() => {'child': child?.toJson()};
/// ```
final bool explicitToJson;

/// Creates an instance of [JsonSerializableGenerator].
///
/// If [typeHelpers] is not provided, three built-in helpers are used:
Expand All @@ -77,9 +97,11 @@ class JsonSerializableGenerator
bool useWrappers: false,
bool anyMap: false,
bool checked: false,
bool explicitToJson: false,
}) : this.useWrappers = useWrappers ?? false,
this.anyMap = anyMap ?? false,
this.checked = checked ?? false,
this.explicitToJson = explicitToJson ?? false,
this._typeHelpers = typeHelpers ?? _defaultHelpers;

/// Creates an instance of [JsonSerializableGenerator].
Expand Down
2 changes: 2 additions & 0 deletions json_serializable/lib/src/type_helper_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {

bool get anyMap => _generator.anyMap;

bool get explicitToJson => _generator.explicitToJson;

@override
bool get nullable => _key.nullable;

Expand Down
6 changes: 5 additions & 1 deletion json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ class JsonHelper extends TypeHelper {
/// By default, JSON encoding in from `dart:convert` calls `toJson()` on
/// provided objects.
@override
String serialize(DartType targetType, String expression, _) {
String serialize(
DartType targetType, String expression, SerializeContext context) {
if (!_canSerialize(targetType)) {
return null;
}

if (context is TypeHelperContext && context.explicitToJson) {
return '$expression${context.nullable ? '?' : ''}.toJson()';
}
return expression;
}

Expand Down
6 changes: 4 additions & 2 deletions json_serializable/test/config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ const _validConfig = const {
'header': 'header',
'use_wrappers': true,
'any_map': true,
'checked': true
'checked': true,
'explicit_to_json': true
};

const _invalidConfig = const {
'header': true,
'use_wrappers': 42,
'any_map': 42,
'checked': 42
'checked': 42,
'explicit_to_json': 42
};
93 changes: 93 additions & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,99 @@ void _registerTests(JsonSerializableGenerator generator) {
_throwsInvalidGenerationSourceError(messageMatcher, todoMatcher));
}

group('explicit toJson', () {
test('nullable', () async {
var output = await _runForElementNamed(
new JsonSerializableGenerator(
explicitToJson: true, useWrappers: generator.useWrappers),
'TrivialNestedNullable');

var expected = generator.useWrappers
? r'''abstract class _$TrivialNestedNullableSerializerMixin {
TrivialNestedNullable get child;
int get otherField;
Map<String, dynamic> toJson() =>
new _$TrivialNestedNullableJsonMapWrapper(this);
}

class _$TrivialNestedNullableJsonMapWrapper extends $JsonMapWrapper {
final _$TrivialNestedNullableSerializerMixin _v;
_$TrivialNestedNullableJsonMapWrapper(this._v);

@override
Iterable<String> get keys => const ['child', 'otherField'];

@override
dynamic operator [](Object key) {
if (key is String) {
switch (key) {
case 'child':
return _v.child?.toJson();
case 'otherField':
return _v.otherField;
}
}
return null;
}
}
'''
: r'''abstract class _$TrivialNestedNullableSerializerMixin {
TrivialNestedNullable get child;
int get otherField;
Map<String, dynamic> toJson() =>
<String, dynamic>{'child': child?.toJson(), 'otherField': otherField};
}
''';

expect(output, expected);
});
test('non-nullable', () async {
var output = await _runForElementNamed(
new JsonSerializableGenerator(
explicitToJson: true, useWrappers: generator.useWrappers),
'TrivialNestedNonNullable');

var expected = generator.useWrappers
? r'''abstract class _$TrivialNestedNonNullableSerializerMixin {
TrivialNestedNonNullable get child;
int get otherField;
Map<String, dynamic> toJson() =>
new _$TrivialNestedNonNullableJsonMapWrapper(this);
}

class _$TrivialNestedNonNullableJsonMapWrapper extends $JsonMapWrapper {
final _$TrivialNestedNonNullableSerializerMixin _v;
_$TrivialNestedNonNullableJsonMapWrapper(this._v);

@override
Iterable<String> get keys => const ['child', 'otherField'];

@override
dynamic operator [](Object key) {
if (key is String) {
switch (key) {
case 'child':
return _v.child.toJson();
case 'otherField':
return _v.otherField;
}
}
return null;
}
}
'''
: r'''abstract class _$TrivialNestedNonNullableSerializerMixin {
TrivialNestedNonNullable get child;
int get otherField;
Map<String, dynamic> toJson() =>
<String, dynamic>{'child': child.toJson(), 'otherField': otherField};
}
''';

expect(output, expected);
});
});

group('non-classes', () {
test('const field', () {
expectThrows('theAnswer', 'Generator cannot target `theAnswer`.',
Expand Down
12 changes: 12 additions & 0 deletions json_serializable/test/src/json_serializable_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,15 @@ class SuperType {
int priceFraction(int other) =>
superTypeViaCtor == null ? null : superTypeViaCtor ~/ other;
}

@JsonSerializable(createFactory: false)
class TrivialNestedNullable {
TrivialNestedNullable child;
int otherField;
}

@JsonSerializable(createFactory: false, nullable: false)
class TrivialNestedNonNullable {
TrivialNestedNonNullable child;
int otherField;
}