Skip to content

Add JsonKey.required, plus other things #221

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
10 changes: 10 additions & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 0.2.8

* 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 `JsonKey.required` field and an associated
`MissingRequiredKeysException` that is thrown when `required` fields don't
have corresponding keys in a source JSON map.

## 0.2.7+1

* Small improvement to `UnrecognizedKeysException.message` output and
Expand Down
49 changes: 43 additions & 6 deletions json_annotation/lib/src/allowed_keys_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Helper function used in generated code when
/// **DEPRECATED** Helper function used in generated code when
/// `JsonSerializable.disallowUnrecognizedKeys` is `true`.
///
/// Should not be used directly.
@Deprecated('Code generated with the latest `json_serializable` will use '
'`\$checkKeys` instead. This function will be removed in the next major '
'release.')
void $checkAllowedKeys(Map map, Iterable<String> allowedKeys) {
if (map == null) return;
var invalidKeys = map.keys.where((k) => !allowedKeys.contains(k));
if (invalidKeys.isNotEmpty) {
throw new UnrecognizedKeysException(
new List<String>.from(invalidKeys), map, allowedKeys.toList());
$checkKeys(map, allowedKeys: allowedKeys?.toList());
}

/// Helper function used in generated `fromJson` code when
/// `JsonSerializable.disallowUnrecognizedKeys` is true for an annotated type or
/// `JsonKey.required` is `true` for any annotated fields.
///
/// Should not be used directly.
void $checkKeys(Map map,
{List<String> allowedKeys, List<String> requiredKeys}) {
if (map != null && allowedKeys != null) {
var invalidKeys =
map.keys.cast<String>().where((k) => !allowedKeys.contains(k)).toList();
if (invalidKeys.isNotEmpty) {
throw new UnrecognizedKeysException(invalidKeys, map, allowedKeys);
}
}

if (requiredKeys != null) {
var missingKeys = requiredKeys.where((k) => !map.keys.contains(k)).toList();
if (missingKeys.isNotEmpty) {
throw new MissingRequiredKeysException(missingKeys, map);
}
}
}

Expand All @@ -34,3 +55,19 @@ class UnrecognizedKeysException implements Exception {

UnrecognizedKeysException(this.unrecognizedKeys, this.map, this.allowedKeys);
}

/// Exception thrown if there are missing required keys in a JSON map that was
/// provided during deserialization.
class MissingRequiredKeysException implements Exception {
/// 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.
String get message => 'Required keys are missing: ${missingKeys.join(', ')}.';

MissingRequiredKeysException(this.missingKeys, this.map)
: assert(missingKeys.isNotEmpty);
}
4 changes: 4 additions & 0 deletions json_annotation/lib/src/checked_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ T $checkedNew<T>(String className, Map map, T constructor(),
String key;
if (error is ArgumentError) {
key = fieldKeyMap[error.name] ?? error.name;
} else if (error is MissingRequiredKeysException) {
key = error.missingKeys.first;
}
throw new CheckedFromJsonException._(error, stack, map, key,
className: className);
Expand Down Expand Up @@ -92,6 +94,8 @@ class CheckedFromJsonException implements Exception {
return error.message?.toString();
} else if (error is UnrecognizedKeysException) {
return error.message;
} else if (error is MissingRequiredKeysException) {
return error.message;
}
return null;
}
Expand Down
13 changes: 12 additions & 1 deletion json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ class JsonKey {
/// value is `null`.
final Object defaultValue;

/// When `true`, generated code for `fromJson` will verify that the source
/// JSON map contains the associated key.
///
/// If the key does not exist, a `MissingRequiredKeysException` exception is
/// thrown.
///
/// Note: only the existence of the key is checked. A key with a `null` value
/// is considered valid.
final bool required;

/// Creates a new [JsonKey] instance.
///
/// Only required when the default behavior is not desired.
Expand All @@ -139,7 +149,8 @@ class JsonKey {
this.ignore,
this.fromJson,
this.toJson,
this.defaultValue});
this.defaultValue,
this.required});
}

// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
Expand Down
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.7+1
version: 0.2.8-dev
description: >-
Classes and helper functions that support JSON code generation via the
`json_serializable` package.
Expand Down
10 changes: 8 additions & 2 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
## 0.5.7

* Added support for `JsonKey.required`.
* When `true`, generated code throws a `MissingRequiredKeysException` if
the key does not exist in the JSON map used to populate the annotated field.
* Will be captured and wrapped in a `CheckedFromJsonException` if
`checked` is enabled in `json_serializable`.

* Added support for `Uri` conversion.

## 0.5.6

* Added support for `JsonSerializable.disallowUnrecognizedKeys`.
* Throws an `UnrecognizedKeysException` if it finds unrecognized keys in the
JSON map used to create the annotated object.
* Will be captured captured and wrapped in a `CheckedFromJsonException` if
JSON map used to populate the annotated field.
* Will be captured and wrapped in a `CheckedFromJsonException` if
`checked` is enabled in `json_serializable`.
* All `fromJson` constructors now use block syntax instead of fat arrows.

Expand Down
42 changes: 27 additions & 15 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,7 @@ class _GeneratorHelper {

fieldsSetByFactory = data.usedCtorParamsAndFields;

if (_annotation.disallowUnrecognizedKeys) {
var listLiteral = jsonLiteralAsDart(
accessibleFields.values.map(_nameAccess).toList(), true);
_buffer.write('''
\$checkAllowedKeys(json, $listLiteral);''');
}
_writeChecks(6, _annotation, accessibleFields);
_buffer.write('''
var val = ${data.content};''');

Expand Down Expand Up @@ -222,15 +217,7 @@ class _GeneratorHelper {

fieldsSetByFactory = data.usedCtorParamsAndFields;

if (_annotation.disallowUnrecognizedKeys) {
var listLiteral = jsonLiteralAsDart(
fieldsSetByFactory
.map((k) => _nameAccess(accessibleFields[k]))
.toList(),
true);
_buffer.write('''
\$checkAllowedKeys(json, $listLiteral);''');
}
_writeChecks(2, _annotation, accessibleFields);

_buffer.write('''
return ${data.content}''');
Expand All @@ -251,6 +238,31 @@ class _GeneratorHelper {
return accessibleFields.values.toSet();
}

void _writeChecks(int indent, JsonSerializable classAnnotation,
Map<String, FieldElement> accessibleFields) {
var args = <String>[];

if (classAnnotation.disallowUnrecognizedKeys) {
var allowKeysLiteral = jsonLiteralAsDart(
accessibleFields.values.map(_nameAccess).toList(), true);

args.add('allowedKeys: $allowKeysLiteral');
}

var requiredKeys =
accessibleFields.values.where((fe) => jsonKeyFor(fe).required).toList();
if (requiredKeys.isNotEmpty) {
var requiredKeyLiteral =
jsonLiteralAsDart(requiredKeys.map(_nameAccess).toList(), true);

args.add('requiredKeys: $requiredKeyLiteral');
}

if (args.isNotEmpty) {
_buffer.writeln('${' ' * indent}\$checkKeys(json, ${args.join(', ')});');
}
}

void _writeWrapper(Iterable<FieldElement> fields) {
_buffer.writeln();
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
Expand Down
19 changes: 14 additions & 5 deletions json_serializable/lib/src/json_key_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ JsonKeyWithConversion _from(FieldElement element) {
obj.getField('includeIfNull').toBoolValue(),
obj.getField('ignore').toBoolValue(),
defaultValueLiteral,
obj.getField('required').toBoolValue(),
fromJsonName,
toJsonName);
}
Expand All @@ -137,16 +138,24 @@ class JsonKeyWithConversion extends JsonKey {
const JsonKeyWithConversion._empty()
: fromJsonData = null,
toJsonData = null,
super();

JsonKeyWithConversion._(String name, bool nullable, bool includeIfNull,
bool ignore, Object defaultValue, this.fromJsonData, this.toJsonData)
super(required: false);

JsonKeyWithConversion._(
String name,
bool nullable,
bool includeIfNull,
bool ignore,
Object defaultValue,
bool required,
this.fromJsonData,
this.toJsonData)
: super(
name: name,
nullable: nullable,
includeIfNull: includeIfNull,
ignore: ignore,
defaultValue: defaultValue);
defaultValue: defaultValue,
required: required ?? false);
}

ConvertData _getFunctionName(
Expand Down
6 changes: 5 additions & 1 deletion json_serializable/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dependencies:

# Use a tight version constraint to ensure that a constraint on
# `json_annotation`. Properly constrains all features it provides.
json_annotation: '>=0.2.7 <0.2.8'
json_annotation: '>=0.2.8 <0.2.9'
meta: ^1.1.0
path: ^1.3.2
source_gen: '>=0.8.1 <0.9.0'
Expand All @@ -25,3 +25,7 @@ dev_dependencies:
logging: ^0.11.3+1
test: ^0.12.3
yaml: ^2.1.13

dependency_overrides:
json_annotation:
path: ../json_annotation
29 changes: 24 additions & 5 deletions json_serializable/test/kitchen_sink/kitchen_sink_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,32 @@ import 'kitchen_sink.non_nullable.wrapped.dart' as nnwrapped
import 'kitchen_sink.wrapped.dart' as wrapped show testFactory, testFromJson;

import 'kitchen_sink_interface.dart';
import 'strict_keys_object.dart';

final _isATypeError = const isInstanceOf<TypeError>();
final _isAUnrecognizedKeysEexception =
const isInstanceOf<UnrecognizedKeysException>();

Matcher _isAUnrecognizedKeysEexception(expectedMessage) => allOf(
const isInstanceOf<UnrecognizedKeysException>(),
new FeatureMatcher<UnrecognizedKeysException>(
'message', (e) => e.message, expectedMessage));

Matcher _isMissingKeyException(expectedMessage) => allOf(
const isInstanceOf<MissingRequiredKeysException>(),
new FeatureMatcher<MissingRequiredKeysException>(
'message', (e) => e.message, expectedMessage));

void main() {
test('valid values covers all keys', () {
expect(_invalidValueTypes.keys, orderedEquals(_validValues.keys));
});

test('required keys', () {
expect(
() => new StrictKeysObject.fromJson({}),
throwsA(_isMissingKeyException(
'Required keys are missing: value, custom_field.')));
});

group('nullable', () {
group('unwrapped', () {
_nullableTests(nullable.testFactory, nullable.testFromJson);
Expand Down Expand Up @@ -241,8 +257,11 @@ Matcher _getMatcher(bool checked, String expectedKey, bool checkedAssignment) {

innerMatcher = _checkedMatcher(expectedKey);
} else {
innerMatcher =
anyOf(isACastError, _isATypeError, _isAUnrecognizedKeysEexception);
innerMatcher = anyOf(
isACastError,
_isATypeError,
_isAUnrecognizedKeysEexception(
'Unrecognized keys: [invalid_key]; supported keys: [value, custom_field]'));

if (checkedAssignment) {
switch (expectedKey) {
Expand All @@ -253,7 +272,7 @@ Matcher _getMatcher(bool checked, String expectedKey, bool checkedAssignment) {
innerMatcher = isArgumentError;
break;
case 'strictKeysObject':
innerMatcher = _isAUnrecognizedKeysEexception;
innerMatcher = _isAUnrecognizedKeysEexception('bob');
break;
case 'intIterable':
case 'datetime-iterable':
Expand Down
3 changes: 2 additions & 1 deletion json_serializable/test/kitchen_sink/strict_keys_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ part 'strict_keys_object.g.dart';
@JsonSerializable(disallowUnrecognizedKeys: true)
class StrictKeysObject extends Object with _$StrictKeysObjectSerializerMixin {
@override
@JsonKey(required: true)
final int value;

@override
@JsonKey(name: 'custom_field')
@JsonKey(name: 'custom_field', required: true)
final String customField;

StrictKeysObject(this.value, this.customField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ part of 'strict_keys_object.dart';
// **************************************************************************

StrictKeysObject _$StrictKeysObjectFromJson(Map json) {
$checkAllowedKeys(json, const ['value', 'custom_field']);
$checkKeys(json,
allowedKeys: const ['value', 'custom_field'],
requiredKeys: const ['value', 'custom_field']);
return new StrictKeysObject(
json['value'] as int, json['custom_field'] as String);
}
Expand Down
1 change: 1 addition & 0 deletions json_serializable/test/yaml/build_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ part 'build_config.g.dart';

@JsonSerializable()
class Config extends Object with _$ConfigSerializerMixin {
@JsonKey(required: true)
final Map<String, Builder> builders;

Config({@required this.builders});
Expand Down
3 changes: 2 additions & 1 deletion json_serializable/test/yaml/build_config.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ part of 'build_config.dart';

Config _$ConfigFromJson(Map json) {
return $checkedNew('Config', json, () {
$checkKeys(json, requiredKeys: const ['builders']);
var val = new Config(
builders: $checkedConvert(
json,
Expand All @@ -29,7 +30,7 @@ abstract class _$ConfigSerializerMixin {

Builder _$BuilderFromJson(Map json) {
return $checkedNew('Builder', json, () {
$checkAllowedKeys(json, const [
$checkKeys(json, allowedKeys: const [
'target',
'import',
'is_optional',
Expand Down
Loading