Skip to content

Support nullable on class annotation, and other cleanup #61

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

Merged
merged 5 commits into from
Oct 30, 2017
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 .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ env:

matrix:
exclude:
- dart: stable
env: PKG=json_serializable TASK=dartanalyzer
- dart: stable
env: PKG=json_serializable TASK=dartfmt

Expand Down
8 changes: 7 additions & 1 deletion json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.2.1

* `JsonSerializable` class annotation
* Added `nullable` field.
* Fixed doc comment.

## 0.2.0

- Moved annotation classes for `JsonSerializable` and `JsonLiteral`.
* Moved annotation classes for `JsonSerializable` and `JsonLiteral`.
44 changes: 31 additions & 13 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@
// BSD-style license that can be found in the LICENSE file.

class JsonSerializable {
// TODO(kevmoo): document these fields
final bool createFactory;
final bool createToJson;

/// Whether the generator should include the this field in the serialized
/// output, even if the value is `null`.
/// Whether the generator should include fields with `null` values in the
/// serialized output.
final bool includeIfNull;

/// When `true` (the default), `null` values are handled gracefully when
/// serializing fields to JSON and when deserializing `null` and nonexistent
/// values from a JSON map.
///
/// Setting to `false` eliminates `null` verification in the generated code,
/// which reduces the code size. Errors may be thrown at runtime if `null`
/// values are encountered, but the original class should also implement
/// `null` runtime validation if it's critical.
final bool nullable;

// TODO(kevmoo): document the constructor
const JsonSerializable(
{bool createFactory: true,
bool createToJson: true,
bool includeIfNull: true})
bool includeIfNull: true,
bool nullable: true})
: this.createFactory = createFactory ?? true,
this.createToJson = createToJson ?? true,
this.includeIfNull = includeIfNull ?? true;
this.includeIfNull = includeIfNull ?? true,
this.nullable = nullable ?? true;
}

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

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

/// [true] if the generator should include the this field in the serialized
/// `true` if the generator should include the this field in the serialized
/// output, even if the value is `null`.
///
/// The default value, `null`, indicates that the behavior should be
Expand All @@ -46,7 +66,5 @@ class JsonKey {
/// Creates a new [JsonKey].
///
/// Only required when the default behavior is not desired.
const JsonKey({this.name, bool nullable: true, bool includeIfNull})
: this.nullable = nullable ?? true,
this.includeIfNull = includeIfNull;
const JsonKey({this.name, this.nullable, this.includeIfNull});
}
2 changes: 1 addition & 1 deletion json_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: json_annotation
version: 0.2.0
version: 0.2.1-dev
description: Annotations for the json_serializable package
homepage: https://github.com/dart-lang/json_serializable
author: Dart Team <misc@dartlang.org>
Expand Down
8 changes: 4 additions & 4 deletions json_serializable/.travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ dart:
dart_task:
# Run the tests -- include the default-skipped presubmit tests
- test: --run-skipped
- dartfmt: sdk
- dartanalyzer

matrix:
exclude:
- dart: stable
include:
- dart: dev
dart_task:
dartfmt: sdk
- dart: dev
dart_task: dartanalyzer

# Only building master means that we don't run two builds for each pull request.
branches:
Expand Down
2 changes: 2 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

* Support the `nullable` field on the `JsonSerializable` class annotation.

## 0.2.4+1

* Throw a more helpful error when a constructor is missing.
Expand Down
76 changes: 47 additions & 29 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,11 @@ class JsonSerializableGenerator

var buffer = new StringBuffer();

if (annotation.read('createFactory').boolValue) {
var toSkip = _writeFactory(buffer, classElement, fields, prefix);
final classAnnotation = _valueForAnnotation(annotation);

if (classAnnotation.createFactory) {
var toSkip = _writeFactory(
buffer, classElement, fields, prefix, classAnnotation.nullable);

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

if (annotation.read('createToJson').boolValue) {
if (classAnnotation.createToJson) {
//
// Generate the mixin class
//
Expand All @@ -143,15 +146,14 @@ class JsonSerializableGenerator
buffer.writeln(' ${field.type} get ${field.name};');
}

var includeIfNull = annotation.read('includeIfNull').boolValue;

buffer.writeln(' Map<String, dynamic> toJson() ');
if (fieldsList.every((e) => _includeIfNull(e, includeIfNull))) {
if (fieldsList
.every((e) => _includeIfNull(e, classAnnotation.includeIfNull))) {
// write simple `toJson` method that includes all keys...
_writeToJsonSimple(buffer, fields.values);
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
} else {
// At least one field should be excluded if null
_writeToJsonWithNullChecks(buffer, fields.values, includeIfNull);
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
}

// end of the mixin class
Expand All @@ -162,7 +164,7 @@ class JsonSerializableGenerator
}

void _writeToJsonWithNullChecks(StringBuffer buffer,
Iterable<FieldElement> fields, bool classIncludeIfNull) {
Iterable<FieldElement> fields, JsonSerializable classAnnotation) {
buffer.writeln('{');

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

if (_includeIfNull(field, classIncludeIfNull)) {
var expression = _serializeField(field, classAnnotation.nullable,
accessOverride: safeFieldAccess);
if (_includeIfNull(field, classAnnotation.includeIfNull)) {
if (directWrite) {
buffer.writeln('$safeJsonKeyString : '
'${_serializeField(field, accessOverride: safeFieldAccess)},');
buffer.writeln('$safeJsonKeyString : $expression,');
} else {
buffer.writeln('$toJsonMapVarName[$safeJsonKeyString] = '
'${_serializeField(field, accessOverride: safeFieldAccess)};');
buffer
.writeln('$toJsonMapVarName[$safeJsonKeyString] = $expression;');
}
} else {
if (directWrite) {
Expand All @@ -208,8 +211,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
}''');
directWrite = false;
}
buffer.writeln('$toJsonMapHelperName($safeJsonKeyString, '
'${_serializeField(field, accessOverride: safeFieldAccess)});');
buffer
.writeln('$toJsonMapHelperName($safeJsonKeyString, $expression);');
}
}

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

void _writeToJsonSimple(StringBuffer buffer, Iterable<FieldElement> fields) {
void _writeToJsonSimple(StringBuffer buffer, Iterable<FieldElement> fields,
bool classSupportNullable) {
buffer.writeln('=> <String, dynamic>{');

var pairs = <String>[];
for (var field in fields) {
pairs.add('${_safeNameAccess(field)}: ${_serializeField(field )}');
pairs.add(
'${_safeNameAccess(field)}: ${_serializeField(field, classSupportNullable )}');
}
buffer.writeAll(pairs, ',\n');

Expand All @@ -235,7 +240,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
StringBuffer buffer,
ClassElement classElement,
Map<String, FieldElement> fields,
String prefix) {
String prefix,
bool classSupportNullable) {
// creating a copy so it can be mutated
var fieldsToSet = new Map<String, FieldElement>.from(fields);
var className = classElement.displayName;
Expand Down Expand Up @@ -303,7 +309,7 @@ void $toJsonMapHelperName(String key, dynamic value) {
buffer.write(' new $className(');
buffer.writeAll(
ctorArguments.map((paramElement) => _deserializeForField(
fields[paramElement.name],
fields[paramElement.name], classSupportNullable,
ctorParam: paramElement)),
', ');
if (ctorArguments.isNotEmpty && ctorNamedArguments.isNotEmpty) {
Expand All @@ -312,7 +318,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
buffer.writeAll(
ctorNamedArguments.map((paramElement) =>
'${paramElement.name}: ' +
_deserializeForField(fields[paramElement.name],
_deserializeForField(
fields[paramElement.name], classSupportNullable,
ctorParam: paramElement)),
', ');

Expand All @@ -323,7 +330,7 @@ void $toJsonMapHelperName(String key, dynamic value) {
for (var field in fieldsToSet.values) {
buffer.writeln();
buffer.write(' ..${field.name} = ');
buffer.write(_deserializeForField(field));
buffer.write(_deserializeForField(field, classSupportNullable));
}
buffer.writeln(';');
}
Expand All @@ -335,10 +342,12 @@ void $toJsonMapHelperName(String key, dynamic value) {
Iterable<TypeHelper> get _allHelpers =>
[_typeHelpers, _coreHelpers].expand((e) => e);

String _serializeField(FieldElement field, {String accessOverride}) {
String _serializeField(FieldElement field, bool classIncludeNullable,
{String accessOverride}) {
accessOverride ??= field.name;
try {
return _serialize(field.type, accessOverride, _nullable(field));
return _serialize(
field.type, accessOverride, _nullable(field, classIncludeNullable));
} on UnsupportedTypeError {
throw new InvalidGenerationSourceError(
'Could not generate `toJson` code for '
Expand All @@ -356,14 +365,15 @@ void $toJsonMapHelperName(String key, dynamic value) {
orElse: () =>
throw new UnsupportedTypeError(targetType, expression));

String _deserializeForField(FieldElement field,
String _deserializeForField(FieldElement field, bool classSupportNullable,
{ParameterElement ctorParam}) {
var jsonKey = _safeNameAccess(field);

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

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

bool _includeIfNull(FieldElement element, bool parentValue) =>
_jsonKeyFor(element).includeIfNull ?? parentValue;
bool _includeIfNull(FieldElement field, bool parentValue) =>
_jsonKeyFor(field).includeIfNull ?? parentValue;

JsonKey _jsonKeyFor(FieldElement element) {
var key = _jsonKeyExpando[element];
Expand All @@ -416,6 +427,13 @@ JsonKey _jsonKeyFor(FieldElement element) {
return key;
}

JsonSerializable _valueForAnnotation(ConstantReader annotation) =>
new JsonSerializable(
createToJson: annotation.read('createToJson').boolValue,
createFactory: annotation.read('createFactory').boolValue,
nullable: annotation.read('nullable').boolValue,
includeIfNull: annotation.read('includeIfNull').boolValue);

final _jsonKeyExpando = new Expando<JsonKey>();

final _jsonKeyChecker = new TypeChecker.fromRuntime(JsonKey);
4 changes: 4 additions & 0 deletions json_serializable/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ dev_dependencies:
collection: ^1.14.0
dart_style: ^1.0.0
test: ^0.12.3

dependency_overrides:
json_annotation:
path: ../json_annotation
2 changes: 2 additions & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
@TestOn('!browser')
library json_serializable.test.json_generator_test;

// TODO(kevmoo): test all flavors of `nullable` - class, fields, etc

import 'dart:async';

import 'package:analyzer/dart/ast/ast.dart';
Expand Down
13 changes: 8 additions & 5 deletions json_serializable/test/kitchen_sink_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import 'package:test/test.dart';

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

import 'test_files/bathtub.dart';
import 'test_files/kitchen_sink.dart';
import 'test_files/kitchen_sink.non_nullable.dart';
import 'test_utils.dart';

void main() {
Expand Down Expand Up @@ -80,12 +80,15 @@ void main() {

group('BathTub', () {
test('with null values fails serialization', () {
expect(() => (new Bathtub()..stringDateTimeMap = null).toJson(),
expect(
() =>
(new KitchenSinkNonNullable()..stringDateTimeMap = null).toJson(),
throwsNoSuchMethodError);
});

test('with empty json fails deserialization', () {
expect(() => new Bathtub.fromJson({}), throwsNoSuchMethodError);
expect(() => new KitchenSinkNonNullable.fromJson({}),
throwsNoSuchMethodError);
});

_sharedTests(
Expand All @@ -95,13 +98,13 @@ void main() {
Iterable<Object> objectIterable,
Iterable<int> intIterable,
Iterable<DateTime> dateTimeIterable}) =>
new Bathtub(
new KitchenSinkNonNullable(
iterable: iterable,
dynamicIterable: dynamicIterable,
objectIterable: objectIterable,
intIterable: intIterable,
dateTimeIterable: dateTimeIterable),
(j) => new Bathtub.fromJson(j));
(j) => new KitchenSinkNonNullable.fromJson(j));
});
}

Expand Down
Loading