Skip to content
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

Add JsonKey.includeFromJson/includeToJson - allow explicit control of serialization #1256

Merged
merged 5 commits into from
Nov 30, 2022
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
3 changes: 3 additions & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 4.8.0-dev

- DEPRECATED `JsonKey.ignore`. Replaced by...
- Added `JsonKey.includeFromJson` and `JsonKey.includeToJson` to allow
fine-grained control of if a field is encoded/decoded.
- Added `JsonSerializable.createPerFieldToJson` which allows generating
a `_$ModelPerFieldToJson`, enabling partial encoding of a model.
- Update `JsonKey` documentation to align with new features in
Expand Down
42 changes: 40 additions & 2 deletions json_annotation/lib/src/json_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,27 @@ class JsonKey {
///
/// If `null` (the default) or `false`, the field will be considered for
/// serialization.
///
/// This field is DEPRECATED use [includeFromJson] and [includeToJson]
/// instead.
@Deprecated(
'Use `includeFromJson` and `includeToJson` with a value of `false` '
'instead.',
)
final bool? ignore;

/// Used to force a field to be included (or excluded) when decoding a object
/// from JSON.
///
/// `null` (the default) means the field will be handled with the default
/// semantics that take into account if it's private or if it can be cleanly
/// round-tripped to-from JSON.
///
/// `true` means the field should always be decoded, even if it's private.
///
/// `false` means the field should never be decoded.
final bool? includeFromJson;

/// Whether the generator should include fields with `null` values in the
/// serialized output.
///
Expand All @@ -66,6 +85,18 @@ class JsonKey {
/// same field, an exception will be thrown during code generation.
final bool? includeIfNull;

/// Used to force a field to be included (or excluded) when encoding a object
/// to JSON.
///
/// `null` (the default) means the field will be handled with the default
/// semantics that take into account if it's private or if it can be cleanly
/// round-tripped to-from JSON.
///
/// `true` means the field should always be encoded, even if it's private.
///
/// `false` means the field should never be encoded.
final bool? includeToJson;

/// The key in a JSON map to use when reading and writing values corresponding
/// to the annotated fields.
///
Expand Down Expand Up @@ -122,12 +153,19 @@ class JsonKey {
///
/// Only required when the default behavior is not desired.
const JsonKey({
@Deprecated('Has no effect') bool? nullable,
@Deprecated('Has no effect')
bool? nullable,
this.defaultValue,
this.disallowNullValue,
this.fromJson,
this.ignore,
@Deprecated(
'Use `includeFromJson` and `includeToJson` with a value of `false` '
'instead.',
)
this.ignore,
this.includeFromJson,
this.includeIfNull,
this.includeToJson,
this.name,
this.readValue,
this.required,
Expand Down
4 changes: 2 additions & 2 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class JsonSerializable {
/// generated.
///
/// It will have the same effect as if those fields had been annotated with
/// `@JsonKey(ignore: true)`.
/// [JsonKey.includeToJson] and [JsonKey.includeFromJson] set to `false`
final bool? ignoreUnannotated;

/// Whether the generator should include fields with `null` values in the
Expand Down Expand Up @@ -237,7 +237,7 @@ class JsonSerializable {
/// @myCustomAnnotation
/// class Another {...}
/// ```
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
final List<JsonConverter>? converters;

/// Creates a new [JsonSerializable] instance.
Expand Down
1 change: 1 addition & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 6.6.0-dev

- Support for `JsonKey.includeFromJson` and `JsonKey.includeToJson`.
- Support `JsonEnum.valueField` being set with `'index'`.
- Require Dart SDK `>=2.18.0`.
- Require `analyzer: ^5.2.0`
Expand Down
13 changes: 13 additions & 0 deletions json_serializable/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ targets:
generate_for:
- example/*
- test/default_value/*
- test/field_matrix_test.field_matrix.dart
- test/generic_files/*
- test/integration/*
- test/kitchen_sink/*
- test/literal/*
- test/supported_types/*
- tool/readme/*

json_serializable|_field_matrix_builder:
generate_for:
- test/field_matrix_test.dart

json_serializable|_test_builder:
generate_for:
- test/default_value/default_value.dart
Expand Down Expand Up @@ -95,6 +100,14 @@ builders:
build_to: source
runs_before: ["json_serializable"]

_field_matrix_builder:
import: 'tool/field_matrix_builder.dart'
builder_factories: ['builder']
build_extensions:
.dart: [.field_matrix.dart]
build_to: source
runs_before: ["json_serializable"]

_readme_builder:
import: "tool/readme_builder.dart"
builder_factories: ["readmeBuilder"]
Expand Down
6 changes: 3 additions & 3 deletions json_serializable/lib/src/field_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ class _FieldSet implements Comparable<_FieldSet> {
}
}

/// Returns a [Set] of all instance [FieldElement] items for [element] and
/// Returns a [List] of all instance [FieldElement] items for [element] and
/// super classes, sorted first by their location in the inheritance hierarchy
/// (super first) and then by their location in the source file.
Iterable<FieldElement> createSortedFieldSet(ClassElement element) {
List<FieldElement> createSortedFieldSet(ClassElement element) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just a small implementation tweak...

// Get all of the fields that need to be assigned
// TODO: support overriding the field set with an annotation option
final elementInstanceFields = Map.fromEntries(
Expand Down Expand Up @@ -104,7 +104,7 @@ Iterable<FieldElement> createSortedFieldSet(ClassElement element) {
.toList()
..sort();

return fields.map((fs) => fs.field).toList();
return fields.map((fs) => fs.field).toList(growable: false);
}

const _dartCoreObjectChecker = TypeChecker.fromRuntime(Object);
74 changes: 52 additions & 22 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import '../type_helper.dart';
import 'decode_helper.dart';
import 'encoder_helper.dart';
import 'field_helpers.dart';
import 'helper_core.dart';
import 'settings.dart';
import 'type_helper.dart';
import 'utils.dart';

class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
Expand Down Expand Up @@ -61,16 +61,17 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
final accessibleFields = sortedFields.fold<Map<String, FieldElement>>(
<String, FieldElement>{},
(map, field) {
if (!field.isPublic) {
final jsonKey = jsonKeyFor(field);
if (!field.isPublic && !jsonKey.explicitYesFromJson) {
unavailableReasons[field.name] = 'It is assigned to a private field.';
} else if (field.getter == null) {
assert(field.setter != null);
unavailableReasons[field.name] =
'Setter-only properties are not supported.';
log.warning('Setters are ignored: ${element.name}.${field.name}');
} else if (jsonKeyFor(field).ignore) {
} else if (jsonKey.explicitNoFromJson) {
unavailableReasons[field.name] =
'It is assigned to an ignored field.';
'It is assigned to a field not meant to be used in fromJson.';
} else {
assert(!map.containsKey(field.name));
map[field.name] = field;
Expand All @@ -85,28 +86,47 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
final createResult = createFactory(accessibleFields, unavailableReasons);
yield createResult.output;

accessibleFieldSet = accessibleFields.entries
final fieldsToUse = accessibleFields.entries
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Needs to be something sortable...

.where((e) => createResult.usedFields.contains(e.key))
.map((e) => e.value)
.toSet();
.toList();

// Need to add candidates BACK even if they are not used in the factory if
// they are forced to be used for toJSON
for (var candidate in sortedFields.where((element) =>
jsonKeyFor(element).explicitYesToJson &&
!fieldsToUse.contains(element))) {
fieldsToUse.add(candidate);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need to add candidates BACK even if they are not used in the factory if they are forced to be used for toJSON

Copy link
Collaborator

Choose a reason for hiding this comment

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

make these comments actual comments?

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!

}

// Need the fields to maintain the original source ordering
fieldsToUse.sort(
(a, b) => sortedFields.indexOf(a).compareTo(sortedFields.indexOf(b)));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need the fields to maintain the original source ordering


accessibleFieldSet = fieldsToUse.toSet();
}

// Check for duplicate JSON keys due to colliding annotations.
// We do this now, since we have a final field list after any pruning done
// by `_writeCtor`.
accessibleFieldSet.fold(
<String>{},
(Set<String> set, fe) {
final jsonKey = nameAccess(fe);
if (!set.add(jsonKey)) {
throw InvalidGenerationSourceError(
'More than one field has the JSON key for name "$jsonKey".',
element: fe,
);
}
return set;
},
);
accessibleFieldSet
..removeWhere(
(element) => jsonKeyFor(element).explicitNoToJson,
)

// Check for duplicate JSON keys due to colliding annotations.
// We do this now, since we have a final field list after any pruning done
// by `_writeCtor`.
..fold(
<String>{},
(Set<String> set, fe) {
final jsonKey = nameAccess(fe);
if (!set.add(jsonKey)) {
throw InvalidGenerationSourceError(
'More than one field has the JSON key for name "$jsonKey".',
element: fe,
);
}
return set;
},
);

if (config.createFieldMap) {
yield createFieldMap(accessibleFieldSet);
Expand All @@ -123,3 +143,13 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
yield* _addedMembers;
}
}

extension on KeyConfig {
bool get explicitYesFromJson => includeFromJson == true;

bool get explicitNoFromJson => includeFromJson == false;

bool get explicitYesToJson => includeToJson == true;

bool get explicitNoToJson => includeFromJson == false;
Copy link

Choose a reason for hiding this comment

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

Shouldn't this be includeToJson == false?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup! Great find! #1281

}
35 changes: 31 additions & 4 deletions json_serializable/lib/src/json_key_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
classAnnotation,
element,
defaultValue: ctorParamDefault,
ignore: classAnnotation.ignoreUnannotated,
includeFromJson: classAnnotation.ignoreUnannotated ? false : null,
includeToJson: classAnnotation.ignoreUnannotated ? false : null,
);
}

Expand Down Expand Up @@ -236,18 +237,42 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
readValue.objectValue.toFunctionValue()!.qualifiedName;
}

final ignore = obj.read('ignore').literalValue as bool?;
var includeFromJson = obj.read('includeFromJson').literalValue as bool?;
var includeToJson = obj.read('includeToJson').literalValue as bool?;

if (ignore != null) {
if (includeFromJson != null) {
throwUnsupported(
element,
'Cannot use both `ignore` and `includeFromJson` on the same field. '
'Since `ignore` is deprecated, you should only use `includeFromJson`.',
);
}
if (includeToJson != null) {
throwUnsupported(
element,
'Cannot use both `ignore` and `includeToJson` on the same field. '
'Since `ignore` is deprecated, you should only use `includeToJson`.',
);
}
assert(includeFromJson == null && includeToJson == null);
includeToJson = includeFromJson = !ignore;
}

return _populateJsonKey(
classAnnotation,
element,
defaultValue: defaultValue ?? ctorParamDefault,
disallowNullValue: obj.read('disallowNullValue').literalValue as bool?,
ignore: obj.read('ignore').literalValue as bool?,
includeIfNull: obj.read('includeIfNull').literalValue as bool?,
name: obj.read('name').literalValue as String?,
readValueFunctionName: readValueFunctionName,
required: obj.read('required').literalValue as bool?,
unknownEnumValue:
createAnnotationValue('unknownEnumValue', mustBeEnum: true),
includeToJson: includeToJson,
includeFromJson: includeFromJson,
);
}

Expand All @@ -256,12 +281,13 @@ KeyConfig _populateJsonKey(
FieldElement element, {
required String? defaultValue,
bool? disallowNullValue,
bool? ignore,
bool? includeIfNull,
String? name,
String? readValueFunctionName,
bool? required,
String? unknownEnumValue,
bool? includeToJson,
bool? includeFromJson,
}) {
if (disallowNullValue == true) {
if (includeIfNull == true) {
Expand All @@ -275,13 +301,14 @@ KeyConfig _populateJsonKey(
return KeyConfig(
defaultValue: defaultValue,
disallowNullValue: disallowNullValue ?? false,
ignore: ignore ?? false,
includeIfNull: _includeIfNull(
includeIfNull, disallowNullValue, classAnnotation.includeIfNull),
name: name ?? encodedFieldName(classAnnotation.fieldRename, element.name),
readValueFunctionName: readValueFunctionName,
required: required ?? false,
unknownEnumValue: unknownEnumValue,
includeFromJson: includeFromJson,
includeToJson: includeToJson,
);
}

Expand Down
7 changes: 5 additions & 2 deletions json_serializable/lib/src/type_helpers/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ class KeyConfig {

final bool disallowNullValue;

final bool ignore;
final bool? includeFromJson;

final bool includeIfNull;

final bool? includeToJson;

final String name;

final bool required;
Expand All @@ -26,8 +28,9 @@ class KeyConfig {
KeyConfig({
required this.defaultValue,
required this.disallowNullValue,
required this.ignore,
required this.includeFromJson,
required this.includeIfNull,
required this.includeToJson,
required this.name,
required this.readValueFunctionName,
required this.required,
Expand Down
Loading