Skip to content

Add JsonKey.disallowNullValue - and related commits #224

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 3 commits into from
Jun 6, 2018
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
8 changes: 8 additions & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
* Added `$checkKeys` helper function and deprecated `$checkAllowedKeys`.
Upgrading to the latest `json_serializable` and re-running your build will
eliminate any `@deprecated` hints you see.

* Added `BadKeyException` exception which is the abstract super class for
`MissingRequiredKeysException`, `UnrecognizedKeysException`, and
`DisallowedNullValueException`.

* Added `JsonKey.required` field and an associated
`MissingRequiredKeysException` that is thrown when `required` fields don't
have corresponding keys in a source JSON map.

* Added `JsonKey.disallowNullValue` field and an associated
`DisallowedNullValueException` that is thrown when corresponding keys exist in
a source JSON map, but their values are `null`.

## 0.2.7+1

Expand Down
61 changes: 47 additions & 14 deletions json_annotation/lib/src/allowed_keys_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ void $checkAllowedKeys(Map map, Iterable<String> allowedKeys) {
///
/// Should not be used directly.
void $checkKeys(Map map,
{List<String> allowedKeys, List<String> requiredKeys}) {
{List<String> allowedKeys,
List<String> requiredKeys,
List<String> disallowNullValues}) {
if (map != null && allowedKeys != null) {
var invalidKeys =
map.keys.cast<String>().where((k) => !allowedKeys.contains(k)).toList();
Expand All @@ -34,40 +36,71 @@ void $checkKeys(Map map,
throw new MissingRequiredKeysException(missingKeys, map);
}
}

if (map != null && disallowNullValues != null) {
var nullValuedKeys = map.entries
.where((entry) =>
disallowNullValues.contains(entry.key) && entry.value == null)
.map((entry) => entry.key as String)
.toList();

if (nullValuedKeys.isNotEmpty) {
throw new DisallowedNullValueException(nullValuedKeys, map);
}
}
}

/// A base class for exceptions thrown when decoding JSON.
abstract class BadKeyException implements Exception {
BadKeyException._(this.map);

/// The source [Map] that the unrecognized keys were found in.
final Map map;

/// A human-readable message corresponding to the error.
String get message;
}

/// Exception thrown if there are unrecognized keys in a JSON map that was
/// provided during deserialization.
class UnrecognizedKeysException implements Exception {
class UnrecognizedKeysException extends BadKeyException {
/// The allowed keys for [map].
final List<String> allowedKeys;

/// The keys from [map] that were unrecognized.
final List<String> unrecognizedKeys;

/// The source [Map] that the unrecognized keys were found in.
final Map map;

/// A human-readable message corresponding to the error.
@override
String get message =>
'Unrecognized keys: [${unrecognizedKeys.join(', ')}]; supported keys: '
'[${allowedKeys.join(', ')}]';

UnrecognizedKeysException(this.unrecognizedKeys, this.map, this.allowedKeys);
UnrecognizedKeysException(this.unrecognizedKeys, Map map, this.allowedKeys)
: super._(map);
}

/// Exception thrown if there are missing required keys in a JSON map that was
/// provided during deserialization.
class MissingRequiredKeysException implements Exception {
class MissingRequiredKeysException extends BadKeyException {
/// The keys that [map] is missing.
final List<String> missingKeys;

/// The source [Map] that the required keys were missing in.
final Map map;

/// A human-readable message corresponding to the error.
@override
String get message => 'Required keys are missing: ${missingKeys.join(', ')}.';

MissingRequiredKeysException(this.missingKeys, this.map)
: assert(missingKeys.isNotEmpty);
MissingRequiredKeysException(this.missingKeys, Map map)
: assert(missingKeys.isNotEmpty),
super._(map);
}

/// Exception thrown if there are keys with disallowed `null` values in a JSON
/// map that was provided during deserialization.
class DisallowedNullValueException extends BadKeyException {
final List<String> keysWithNullValues;

DisallowedNullValueException(this.keysWithNullValues, Map map) : super._(map);

@override
String get message => 'These keys had `null` values, '
'which is not allowed: $keysWithNullValues';
}
6 changes: 3 additions & 3 deletions json_annotation/lib/src/checked_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ T $checkedNew<T>(String className, Map map, T constructor(),
key = fieldKeyMap[error.name] ?? error.name;
} else if (error is MissingRequiredKeysException) {
key = error.missingKeys.first;
} else if (error is DisallowedNullValueException) {
key = error.keysWithNullValues.first;
}
throw new CheckedFromJsonException._(error, stack, map, key,
className: className);
Expand Down Expand Up @@ -92,9 +94,7 @@ class CheckedFromJsonException implements Exception {
static String _getMessage(Object error) {
if (error is ArgumentError) {
return error.message?.toString();
} else if (error is UnrecognizedKeysException) {
return error.message;
} else if (error is MissingRequiredKeysException) {
} else if (error is BadKeyException) {
return error.message;
}
return null;
Expand Down
59 changes: 43 additions & 16 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class JsonSerializable {

/// Whether the generator should include fields with `null` values in the
/// serialized output.
///
/// If `true` (the default), all fields are written to JSON, even if they are
/// `null`.
///
/// If a field is annotated with `JsonKey` with a non-`null` value for
/// `includeIfNull`, that value takes precedent.
final bool includeIfNull;

/// When `true` (the default), `null` values are handled gracefully when
Expand All @@ -53,13 +59,13 @@ class JsonSerializable {
final bool nullable;

/// Creates a new [JsonSerializable] instance.
const JsonSerializable(
{bool disallowUnrecognizedKeys: false,
bool createFactory: true,
bool createToJson: true,
bool includeIfNull: true,
bool nullable: true})
: this.disallowUnrecognizedKeys = disallowUnrecognizedKeys ?? false,
const JsonSerializable({
bool disallowUnrecognizedKeys: false,
bool createFactory: true,
bool createToJson: true,
bool includeIfNull: true,
bool nullable: true,
}) : this.disallowUnrecognizedKeys = disallowUnrecognizedKeys ?? false,
this.createFactory = createFactory ?? true,
this.createToJson = createToJson ?? true,
this.includeIfNull = includeIfNull ?? true,
Expand Down Expand Up @@ -94,6 +100,12 @@ class JsonKey {
/// The default value, `null`, indicates that the behavior should be
/// acquired from the [JsonSerializable.includeIfNull] annotation on the
/// enclosing class.
///
/// If [disallowNullValue] is `true`, this value is treated as `false` to
/// ensure compatibility between `toJson` and `fromJson`.
///
/// If both [includeIfNull] and [disallowNullValue] are set to `true` on the
/// same field, an exception will be thrown during code generation.
final bool includeIfNull;

/// `true` if the generator should ignore this field completely.
Expand Down Expand Up @@ -139,18 +151,33 @@ class JsonKey {
/// is considered valid.
final bool required;

/// If `true`, generated code will throw a `DisallowedNullValueException` if
/// the corresponding key exits, but the value is `null`.
///
/// Note: this value does not affect the behavior of a JSON map *without* the
/// associated key.
///
/// If [disallowNullValue] is `true`, [includeIfNull] will be treated as
/// `false` to ensure compatibility between `toJson` and `fromJson`.
///
/// If both [includeIfNull] and [disallowNullValue] are set to `true` on the
/// same field, an exception will be thrown during code generation.
final bool disallowNullValue;

/// Creates a new [JsonKey] instance.
///
/// Only required when the default behavior is not desired.
const JsonKey(
{this.name,
this.nullable,
this.includeIfNull,
this.ignore,
this.fromJson,
this.toJson,
this.defaultValue,
this.required});
const JsonKey({
this.name,
this.nullable,
this.includeIfNull,
this.ignore,
this.fromJson,
this.toJson,
this.defaultValue,
this.required,
this.disallowNullValue,
});
}

// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
Expand Down
6 changes: 6 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
* Will be captured and wrapped in a `CheckedFromJsonException` if
`checked` is enabled in `json_serializable`.

* Added `JsonKey.disallowNullValue`.
* When `true`, generated code throws a `DisallowedNullValueException` if
the corresponding keys exist in in the JSON map, but it's value is null.
* Will be captured and wrapped in a `CheckedFromJsonException` if
`checked` is enabled in `json_serializable`.

* Added support for `Uri` conversion.

## 0.5.6
Expand Down
10 changes: 10 additions & 0 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ class _GeneratorHelper {
args.add('requiredKeys: $requiredKeyLiteral');
}

var disallowNullKeys = accessibleFields.values
.where((fe) => _jsonKeyFor(fe).disallowNullValue)
.toList();
if (disallowNullKeys.isNotEmpty) {
var dissallowNullKeyLiteral =
jsonLiteralAsDart(disallowNullKeys.map(_nameAccess).toList(), true);

args.add('disallowNullValues: $dissallowNullKeyLiteral');
}

if (args.isNotEmpty) {
_buffer.writeln('${' ' * indent}\$checkKeys(json, ${args.join(', ')});');
}
Expand Down
33 changes: 30 additions & 3 deletions json_serializable/lib/src/json_key_with_conversion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,26 @@ JsonKeyWithConversion _from(
}
}

var disallowNullValue = obj.getField('disallowNullValue').toBoolValue();
var includeIfNull = obj.getField('includeIfNull').toBoolValue();

if (disallowNullValue == true) {
if (includeIfNull == true) {
throwUnsupported(
element,
'Cannot set both `disallowNullvalue` and `includeIfNull` to `true`. '
'This leads to incompatible `toJson` and `fromJson` behavior.');
}
}

return new JsonKeyWithConversion._(classAnnotation,
name: obj.getField('name').toStringValue(),
nullable: obj.getField('nullable').toBoolValue(),
includeIfNull: obj.getField('includeIfNull').toBoolValue(),
includeIfNull: includeIfNull,
ignore: obj.getField('ignore').toBoolValue(),
defaultValue: defaultValueLiteral,
required: obj.getField('required').toBoolValue(),
disallowNullValue: disallowNullValue,
fromJsonData: fromJsonName,
toJsonData: toJsonName);
}
Expand Down Expand Up @@ -131,15 +144,29 @@ class JsonKeyWithConversion extends JsonKey {
bool ignore,
Object defaultValue,
bool required,
bool disallowNullValue,
this.fromJsonData,
this.toJsonData,
}) : super(
name: name,
nullable: nullable ?? classAnnotation.nullable,
includeIfNull: includeIfNull ?? classAnnotation.includeIfNull,
includeIfNull: _includeIfNull(includeIfNull, disallowNullValue,
classAnnotation.includeIfNull),
ignore: ignore ?? false,
defaultValue: defaultValue,
required: required ?? false);
required: required ?? false,
disallowNullValue: disallowNullValue ?? false) {
assert(!this.includeIfNull || !this.disallowNullValue);
}

static bool _includeIfNull(bool keyIncludeIfNull, bool keyDisallowNullValue,
bool classIncludeIfNull) {
if (keyDisallowNullValue == true) {
assert(keyIncludeIfNull != true);
return false;
}
return keyIncludeIfNull ?? classIncludeIfNull;
}
}

ConvertData _getFunctionName(
Expand Down
8 changes: 8 additions & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -663,5 +663,13 @@ abstract class _$SubTypeSerializerMixin {
'Cannot use `defaultValue` on a field with `nullable` false.');
});
});

test('`disallowNullvalue` and `includeIfNull` both `true`', () {
expectThrows(
'IncludeIfNullDisallowNullClass',
'Error with `@JsonKey` on `field`. '
'Cannot set both `disallowNullvalue` and `includeIfNull` to `true`. '
'This leads to incompatible `toJson` and `fromJson` behavior.');
});
}
}
6 changes: 6 additions & 0 deletions json_serializable/test/src/json_serializable_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ class PrivateFieldCtorClass {
PrivateFieldCtorClass(this._privateField);
}

@JsonSerializable()
class IncludeIfNullDisallowNullClass {
@JsonKey(includeIfNull: true, disallowNullValue: true)
int field;
}

@JsonSerializable()
class SubType extends SuperType {
final int subTypeViaCtor;
Expand Down
2 changes: 2 additions & 0 deletions json_serializable/test/test_files/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ enum Category { top, bottom, strange, charmed, up, down }

@JsonSerializable()
class Order extends Object with _$OrderSerializerMixin {
/// Used to test that `disallowNullValues: true` forces `includeIfNull: false`
@JsonKey(disallowNullValue: true)
int count;
bool isRushed;

Expand Down
28 changes: 19 additions & 9 deletions json_serializable/test/test_files/json_test_example.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ abstract class _$PersonSerializerMixin {
}

Order _$OrderFromJson(Map<String, dynamic> json) {
$checkKeys(json, disallowNullValues: const ['count']);
return new Order(
$enumDecode('Category', Category.values, json['category'] as String),
(json['items'] as List)?.map((e) =>
Expand All @@ -70,15 +71,24 @@ abstract class _$OrderSerializerMixin {
Platform get platform;
Map<String, Platform> get altPlatforms;
Uri get homepage;
Map<String, dynamic> toJson() => <String, dynamic>{
'count': count,
'isRushed': isRushed,
'category': category.toString().split('.').last,
'items': items,
'platform': platform,
'altPlatforms': altPlatforms,
'homepage': homepage?.toString()
};
Map<String, dynamic> toJson() {
var val = <String, dynamic>{};

void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}

writeNotNull('count', count);
val['isRushed'] = isRushed;
val['category'] = category.toString().split('.').last;
val['items'] = items;
val['platform'] = platform;
val['altPlatforms'] = altPlatforms;
val['homepage'] = homepage?.toString();
return val;
}
}

Item _$ItemFromJson(Map<String, dynamic> json) {
Expand Down
Loading