Skip to content

Optional nullable #23

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 9 commits into from
Jul 24, 2017
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ dart:
- dev
- stable
dart_task:
- test
# Run the tests -- include the default-skipped presubmit tests
- test: --run-skipped
- dartfmt
- dartanalyzer

Expand Down
15 changes: 10 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@

* `package:json_serializable/generators.dart` contains the `Generator`
implementations.

* `package:json_serializable/annotations.dart` contains the annotations.
This library should be imported with your target classes.

* `package:json_serializable/type_helpers.dart` contains `TypeHelper` classes
which allows custom generation for specific types.

* **BREAKING** Fail generation for types that are not a JSON primitive or that
do not explicitly supports JSON serialization.

* **BREAKING** `TypeHelper`:

* Removed `can` methods. Return `null` from `(de)serialize` if the provided
type is not supported.

* Added `(de)serializeNested` arguments to `(de)serialize` methods allowing
generic types. This is (now) how support for `Iterable`, `List`, and `Map`
is implemented.


* **BREAKING** `JsonKey.jsonName` was renamed to `name` and is now a named
parameter.

* Added support for optional, non-nullable fields.

* Eliminated all implicit casts in generated code. These would end up being
runtime checks in most cases.

Expand Down
3 changes: 3 additions & 0 deletions dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tags:
presubmit-only:
skip: "Should only be run during presubmit"
11 changes: 9 additions & 2 deletions example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ part 'example.g.dart';
class Person extends Object with _$PersonSerializerMixin {
final String firstName, middleName, lastName;

@JsonKey('date-of-birth')
@JsonKey(name: 'date-of-birth', nullable: false)
final DateTime dateOfBirth;

@JsonKey(name: 'last-order')
final DateTime lastOrder;

@JsonKey(nullable: false)
List<Order> orders;

Person(this.firstName, this.lastName, {this.middleName, this.dateOfBirth});
Person(this.firstName, this.lastName, this.dateOfBirth,
{this.middleName, this.lastOrder, List<Order> orders})
: this.orders = orders ?? <Order>[];

factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}
Expand Down
25 changes: 14 additions & 11 deletions example/example.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 20 additions & 6 deletions lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ class JsonSerializable {
this.createToJson = createToJson;
}

/// Customizes the name of the JSON key for a field.
///
/// If omitted, the resulting JSON key will be the
/// name of the field defined on the class.
/// Customizes the how an annotated field is serialized
class JsonKey {
final String jsonName;
const JsonKey(this.jsonName);
/// The key in a JSON map to use when reading and writing values corresponding
/// to the annotated fields.
///
/// If `null`, the field name is used.
final String name;

/// [true] if the generator should validate all values for `null` in
/// serialization code.
///
/// 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.
final bool nullable;

/// Creates a new [JsonKey].
///
/// Only required when the default behavior is not desired.
const JsonKey({this.name, bool nullable: true})
: this.nullable = nullable ?? true;
}
58 changes: 35 additions & 23 deletions lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ class JsonSerializableGenerator
var pairs = <String>[];
fields.forEach((fieldName, field) {
try {
pairs.add("'${_fieldToJsonMapKey(fieldName, field)}': "
"${_serialize(field.type, fieldName )}");
pairs.add("'${_fieldToJsonMapKey(field) ?? fieldName}': "
"${_serialize(field.type, fieldName , _nullable(field))}");
} on UnsupportedTypeError {
throw new InvalidGenerationSourceError(
"Could not generate `toJson` code for `${friendlyNameForElement(field)}`.",
Expand Down Expand Up @@ -244,42 +244,54 @@ class JsonSerializableGenerator

/// [expression] may be just the name of the field or it may an expression
/// representing the serialization of a value.
String _serialize(DartType targetType, String expression) => _allHelpers
.map((h) => h.serialize(targetType, expression, _serialize))
.firstWhere((r) => r != null,
orElse: () => throw new UnsupportedTypeError(targetType, expression));
String _serialize(DartType targetType, String expression, bool nullable) =>
_allHelpers
.map((h) => h.serialize(targetType, expression, nullable, _serialize))
.firstWhere((r) => r != null,
orElse: () =>
throw new UnsupportedTypeError(targetType, expression));

String _deserializeForField(String name, FieldElement field,
{ParameterElement ctorParam}) {
name = _fieldToJsonMapKey(name, field);
name = _fieldToJsonMapKey(field) ?? name;

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

try {
return _deserialize(targetType, "json['$name']");
return _deserialize(targetType, "json['$name']", _nullable(field));
} on UnsupportedTypeError {
throw new InvalidGenerationSourceError(
"Could not generate fromJson code for `${friendlyNameForElement(field)}`.",
todo: "Make sure all of the types are serializable.");
}
}

String _deserialize(DartType targetType, String expression) => _allHelpers
.map((th) => th.deserialize(targetType, expression, _deserialize))
.firstWhere((r) => r != null,
orElse: () => throw new UnsupportedTypeError(targetType, expression));
String _deserialize(DartType targetType, String expression, bool nullable) =>
_allHelpers
.map((th) =>
th.deserialize(targetType, expression, nullable, _deserialize))
.firstWhere((r) => r != null,
orElse: () =>
throw new UnsupportedTypeError(targetType, expression));
}

/// Returns the JSON map `key` to be used when (de)serializing [field].
/// Returns the JSON map `key` to be used when (de)serializing [field], if any.
///
/// [fieldName] is used, unless [field] is annotated with [JsonKey], in which
/// case [JsonKey.jsonName] is used.
String _fieldToJsonMapKey(String fieldName, FieldElement field) {
const $JsonKey = const TypeChecker.fromRuntime(JsonKey);
var jsonKey = $JsonKey.firstAnnotationOf(field);
if (jsonKey != null) {
var jsonName = jsonKey.getField('jsonName').toStringValue();
return jsonName;
}
return fieldName;
/// Otherwise, `null`;
String _fieldToJsonMapKey(FieldElement field) =>
_getJsonKeyReader(field)?.read('name')?.anyValue as String;

/// 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) =>
_getJsonKeyReader(field)?.read('nullable')?.boolValue ?? true;

ConstantReader _getJsonKeyReader(FieldElement element) {
var obj = _jsonKeyChecker.firstAnnotationOfExact(element) ??
_jsonKeyChecker.firstAnnotationOfExact(element.getter);

return obj == null ? null : new ConstantReader(obj);
}

final _jsonKeyChecker = new TypeChecker.fromRuntime(JsonKey);
11 changes: 7 additions & 4 deletions lib/src/type_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ List<DartType> typeArgumentsOf(DartType type, TypeChecker checker) {
return implementation?.typeArguments;
}

typedef String TypeHelperGenerator(
DartType fieldType, String expression, bool nullable);

abstract class TypeHelper {
const TypeHelper();

Expand All @@ -34,8 +37,8 @@ abstract class TypeHelper {
/// "$expression.id";
/// ```.
// TODO(kevmoo) – document `serializeNested`
String serialize(DartType targetType, String expression,
String serializeNested(DartType t, String e));
String serialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator serializeNested);

/// Returns Dart code that deserializes an [expression] representing a JSON
/// literal to into [targetType].
Expand All @@ -61,8 +64,8 @@ abstract class TypeHelper {
/// "new ${targetType.name}.fromInt($expression)";
/// ```.
// TODO(kevmoo) – document `deserializeNested`
String deserialize(DartType targetType, String expression,
String deserializeNested(DartType t, String e));
String deserialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator deserializeNested);
}

/// A [TypeChecker] for [String], [bool] and [num].
Expand Down
38 changes: 29 additions & 9 deletions lib/src/type_helpers/date_time_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,37 @@ class DateTimeHelper extends TypeHelper {
const DateTimeHelper();

@override
String serialize(DartType targetType, String expression, _) =>
_matchesType(targetType) ? "$expression?.toIso8601String()" : null;
String serialize(DartType targetType, String expression, bool nullable, _) {
if (!_matchesType(targetType)) {
return null;
}

var buffer = new StringBuffer(expression);

if (nullable) {
buffer.write('?');
}

buffer.write(".toIso8601String()");

return buffer.toString();
}

@override
String deserialize(DartType targetType, String expression, _) =>
_matchesType(targetType)
?
// TODO(kevmoo) `String` here is ignoring
// github.com/dart-lang/json_serializable/issues/19
"$expression == null ? null : DateTime.parse($expression as String)"
: null;
String deserialize(DartType targetType, String expression, bool nullable, _) {
if (!_matchesType(targetType)) {
return null;
}

var buffer = new StringBuffer();

if (nullable) {
buffer.write("$expression == null ? null : ");
}

buffer.write("DateTime.parse($expression as String)");
return buffer.toString();
}
}

bool _matchesType(DartType type) =>
Expand Down
29 changes: 18 additions & 11 deletions lib/src/type_helpers/iterable_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class IterableHelper extends TypeHelper {
const IterableHelper();

@override
String serialize(DartType targetType, String expression,
String serializeNested(DartType dartType, String expression)) {
String serialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator serializeNested) {
if (!_coreIterableChecker.isAssignableFromType(targetType)) {
return null;
}
Expand All @@ -19,15 +19,18 @@ class IterableHelper extends TypeHelper {
// Although it's possible that child elements may be marked unsafe

var isList = _coreListChecker.isAssignableFromType(targetType);
var subFieldValue =
serializeNested(_getIterableGenericType(targetType), _closureArg);
var subFieldValue = serializeNested(
_getIterableGenericType(targetType), _closureArg, nullable);

var optionalQuestion = nullable ? '?' : '';

// In the case of trivial JSON types (int, String, etc), `subFieldValue`
// will be identical to `substitute` – so no explicit mapping is needed.
// If they are not equal, then we to write out the substitution.
if (subFieldValue != _closureArg) {
// TODO: the type could be imported from a library with a prefix!
expression = "${expression}?.map(($_closureArg) => $subFieldValue)";
expression =
"${expression}${optionalQuestion}.map(($_closureArg) => $subFieldValue)";

// expression now represents an Iterable (even if it started as a List
// ...resetting `isList` to `false`.
Expand All @@ -36,32 +39,36 @@ class IterableHelper extends TypeHelper {

if (!isList) {
// If the static type is not a List, generate one.
expression += "?.toList()";
expression += "${optionalQuestion}.toList()";
}

return expression;
}

@override
String deserialize(DartType targetType, String expression,
String deserializeNested(DartType t, String e)) {
String deserialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator deserializeNested) {
if (!_coreIterableChecker.isAssignableFromType(targetType)) {
return null;
}

var iterableGenericType = _getIterableGenericType(targetType);

var itemSubVal = deserializeNested(iterableGenericType, _closureArg);
var itemSubVal =
deserializeNested(iterableGenericType, _closureArg, nullable);

// If `itemSubVal` is the same, then we don't need to do anything fancy
if (_closureArg == itemSubVal) {
return '$expression as List';
}

var output = "($expression as List)?.map(($_closureArg) => $itemSubVal)";
var optionalQuestion = nullable ? '?' : '';

var output =
"($expression as List)${optionalQuestion}.map(($_closureArg) => $itemSubVal)";

if (_coreListChecker.isAssignableFromType(targetType)) {
output += "?.toList()";
output += "${optionalQuestion}.toList()";
}

return output;
Expand Down
Loading