Skip to content

Add option for implicitToJson #195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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 @@ -4,6 +4,8 @@

* `enum` deserialization now uses helpers provided by `json_annotation`.

* Added `implicit_to_json` configuration option.

* Small change to how nullable `Map` values are deserialized.

* Small whitespace changes to `JsonLiteral` generation to align with `dartfmt`.
Expand Down
9 changes: 5 additions & 4 deletions json_serializable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ targets:
# Options configure how source code is generated for every
# `@JsonSerializable`-annotated class in the package.
#
# The default value for each of them: `false`.
# The default value for each is shown.
#
# For usage information, reference the corresponding field in
# `JsonSerializableGenerator`.
use_wrappers: true
any_map: true
checked: true
use_wrappers: false
any_map: false
checked: false
implicit_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,
implicitToJson: optionsMap.remove('implicit_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 implicitToJson: true,
}) {
return new PartBuilder([
new JsonSerializableGenerator(
useWrappers: useWrappers, anyMap: anyMap, checked: checked),
useWrappers: useWrappers,
anyMap: anyMap,
checked: checked,
implicitToJson: implicitToJson,
),
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 @@ -66,6 +66,26 @@ class JsonSerializableGenerator
/// [CheckedFromJsonException] is thrown.
final bool checked;

/// If `false`, 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
/// (`implicitToJson: true`) is to omit the `toJson` call.
///
/// Example of `implicitToJson: true` (default)
///
/// ```dart
/// Map<String, dynamic> toJson() => {'child': child};
/// ```
///
/// Example of `implicitToJson: false`
///
/// ```dart
/// Map<String, dynamic> toJson() => {'child': child?.toJson()};
/// ```
final bool implicitToJson;

/// Creates an instance of [JsonSerializableGenerator].
///
/// If [typeHelpers] is not provided, two built-in helpers are used:
Expand All @@ -75,9 +95,11 @@ class JsonSerializableGenerator
bool useWrappers: false,
bool anyMap: false,
bool checked: false,
bool implicitToJson: true,
}) : this.useWrappers = useWrappers ?? false,
this.anyMap = anyMap ?? false,
this.checked = checked ?? false,
this.implicitToJson = implicitToJson ?? true,
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 @@ -28,6 +28,8 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {
// Consider exposing it if there is interest
bool get anyMap => _generator.anyMap;

bool get implicitToJson => _generator.implicitToJson;

TypeHelperContext(this._generator, this.metadata, this.nullable,
this.fromJsonData, this.toJsonData);

Expand Down
17 changes: 12 additions & 5 deletions json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@ import '../utils.dart';
class JsonHelper extends TypeHelper {
const JsonHelper();

/// Simply returns the [expression] provided.
/// If the builder is configured with `implicit_to_json: true`, simply returns
/// the [expression] provided.
///
/// By default, JSON encoding in from `dart:convert` calls `toJson()` on
/// provided objects.
/// Otherwise, returns [expression]`.toJson()` with proper `null` handling.
@override
String serialize(DartType targetType, String expression, _) {
String serialize(
DartType targetType, String expression, SerializeContext context) {
if (!_canSerialize(targetType)) {
return null;
}

return expression;
var ctx = context as TypeHelperContext;

if (ctx.implicitToJson) {
return expression;
}

return '$expression${context.nullable ? '?' : ''}.toJson()';
}

@override
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,
'implicit_to_json': false
};

const _invalidConfig = const {
'header': true,
'use_wrappers': 42,
'any_map': 42,
'checked': 42
'checked': 42,
'implicit_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('implicitToJson: false', () {
test('nullable', () async {
var output = await _runForElementNamed(
new JsonSerializableGenerator(
implicitToJson: false, 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(
implicitToJson: false, 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 @@ -214,3 +214,15 @@ class SuperType {
int priceFraction(int other) =>
superTypeViaCtor == null ? null : superTypeViaCtor ~/ other;
}

@JsonSerializable(createFactory: false)
class TrivialNestedNullable {
TrivialNestedNullable child;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding an int or some other non-custom type here, we want to make sure we don't call toJson on those

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure...the code paths for this are so crazy independent, I'm not too worried – but paranoia is good!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also prevents future regressions etc, looking at this PR alone its not clear this has the correct behavior today.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

int otherField;
}

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