Skip to content

Commit 776df7c

Browse files
authored
Add JsonKey.includeFromJson/includeToJson - allow explicit control of serialization (#1256)
Deprecate `JsonKey.ignore` Fixes #24 Fixes #274 Fixes #537 Fixes #569 Fixes #797 Fixes #1100 Fixes #1244
1 parent 7638402 commit 776df7c

20 files changed

+1046
-55
lines changed

json_annotation/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 4.8.0-dev
22

3+
- DEPRECATED `JsonKey.ignore`. Replaced by...
4+
- Added `JsonKey.includeFromJson` and `JsonKey.includeToJson` to allow
5+
fine-grained control of if a field is encoded/decoded.
36
- Added `JsonSerializable.createPerFieldToJson` which allows generating
47
a `_$ModelPerFieldToJson`, enabling partial encoding of a model.
58
- Update `JsonKey` documentation to align with new features in

json_annotation/lib/src/json_key.dart

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,27 @@ class JsonKey {
4747
///
4848
/// If `null` (the default) or `false`, the field will be considered for
4949
/// serialization.
50+
///
51+
/// This field is DEPRECATED use [includeFromJson] and [includeToJson]
52+
/// instead.
53+
@Deprecated(
54+
'Use `includeFromJson` and `includeToJson` with a value of `false` '
55+
'instead.',
56+
)
5057
final bool? ignore;
5158

59+
/// Used to force a field to be included (or excluded) when decoding a object
60+
/// from JSON.
61+
///
62+
/// `null` (the default) means the field will be handled with the default
63+
/// semantics that take into account if it's private or if it can be cleanly
64+
/// round-tripped to-from JSON.
65+
///
66+
/// `true` means the field should always be decoded, even if it's private.
67+
///
68+
/// `false` means the field should never be decoded.
69+
final bool? includeFromJson;
70+
5271
/// Whether the generator should include fields with `null` values in the
5372
/// serialized output.
5473
///
@@ -66,6 +85,18 @@ class JsonKey {
6685
/// same field, an exception will be thrown during code generation.
6786
final bool? includeIfNull;
6887

88+
/// Used to force a field to be included (or excluded) when encoding a object
89+
/// to JSON.
90+
///
91+
/// `null` (the default) means the field will be handled with the default
92+
/// semantics that take into account if it's private or if it can be cleanly
93+
/// round-tripped to-from JSON.
94+
///
95+
/// `true` means the field should always be encoded, even if it's private.
96+
///
97+
/// `false` means the field should never be encoded.
98+
final bool? includeToJson;
99+
69100
/// The key in a JSON map to use when reading and writing values corresponding
70101
/// to the annotated fields.
71102
///
@@ -122,12 +153,19 @@ class JsonKey {
122153
///
123154
/// Only required when the default behavior is not desired.
124155
const JsonKey({
125-
@Deprecated('Has no effect') bool? nullable,
156+
@Deprecated('Has no effect')
157+
bool? nullable,
126158
this.defaultValue,
127159
this.disallowNullValue,
128160
this.fromJson,
129-
this.ignore,
161+
@Deprecated(
162+
'Use `includeFromJson` and `includeToJson` with a value of `false` '
163+
'instead.',
164+
)
165+
this.ignore,
166+
this.includeFromJson,
130167
this.includeIfNull,
168+
this.includeToJson,
131169
this.name,
132170
this.readValue,
133171
this.required,

json_annotation/lib/src/json_serializable.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ class JsonSerializable {
193193
/// generated.
194194
///
195195
/// It will have the same effect as if those fields had been annotated with
196-
/// `@JsonKey(ignore: true)`.
196+
/// [JsonKey.includeToJson] and [JsonKey.includeFromJson] set to `false`
197197
final bool? ignoreUnannotated;
198198

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

243243
/// Creates a new [JsonSerializable] instance.

json_serializable/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 6.6.0-dev
22

3+
- Support for `JsonKey.includeFromJson` and `JsonKey.includeToJson`.
34
- Support `JsonEnum.valueField` being set with `'index'`.
45
- Require Dart SDK `>=2.18.0`.
56
- Require `analyzer: ^5.2.0`

json_serializable/build.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ targets:
1313
generate_for:
1414
- example/*
1515
- test/default_value/*
16+
- test/field_matrix_test.field_matrix.dart
1617
- test/generic_files/*
1718
- test/integration/*
1819
- test/kitchen_sink/*
1920
- test/literal/*
2021
- test/supported_types/*
2122
- tool/readme/*
2223

24+
json_serializable|_field_matrix_builder:
25+
generate_for:
26+
- test/field_matrix_test.dart
27+
2328
json_serializable|_test_builder:
2429
generate_for:
2530
- test/default_value/default_value.dart
@@ -95,6 +100,14 @@ builders:
95100
build_to: source
96101
runs_before: ["json_serializable"]
97102

103+
_field_matrix_builder:
104+
import: 'tool/field_matrix_builder.dart'
105+
builder_factories: ['builder']
106+
build_extensions:
107+
.dart: [.field_matrix.dart]
108+
build_to: source
109+
runs_before: ["json_serializable"]
110+
98111
_readme_builder:
99112
import: "tool/readme_builder.dart"
100113
builder_factories: ["readmeBuilder"]

json_serializable/lib/src/field_helpers.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ class _FieldSet implements Comparable<_FieldSet> {
6969
}
7070
}
7171

72-
/// Returns a [Set] of all instance [FieldElement] items for [element] and
72+
/// Returns a [List] of all instance [FieldElement] items for [element] and
7373
/// super classes, sorted first by their location in the inheritance hierarchy
7474
/// (super first) and then by their location in the source file.
75-
Iterable<FieldElement> createSortedFieldSet(ClassElement element) {
75+
List<FieldElement> createSortedFieldSet(ClassElement element) {
7676
// Get all of the fields that need to be assigned
7777
// TODO: support overriding the field set with an annotation option
7878
final elementInstanceFields = Map.fromEntries(
@@ -104,7 +104,7 @@ Iterable<FieldElement> createSortedFieldSet(ClassElement element) {
104104
.toList()
105105
..sort();
106106

107-
return fields.map((fs) => fs.field).toList();
107+
return fields.map((fs) => fs.field).toList(growable: false);
108108
}
109109

110110
const _dartCoreObjectChecker = TypeChecker.fromRuntime(Object);

json_serializable/lib/src/generator_helper.dart

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import 'package:analyzer/dart/element/element.dart';
66
import 'package:build/build.dart';
77
import 'package:source_gen/source_gen.dart';
88

9+
import '../type_helper.dart';
910
import 'decode_helper.dart';
1011
import 'encoder_helper.dart';
1112
import 'field_helpers.dart';
1213
import 'helper_core.dart';
1314
import 'settings.dart';
14-
import 'type_helper.dart';
1515
import 'utils.dart';
1616

1717
class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
@@ -61,16 +61,17 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
6161
final accessibleFields = sortedFields.fold<Map<String, FieldElement>>(
6262
<String, FieldElement>{},
6363
(map, field) {
64-
if (!field.isPublic) {
64+
final jsonKey = jsonKeyFor(field);
65+
if (!field.isPublic && !jsonKey.explicitYesFromJson) {
6566
unavailableReasons[field.name] = 'It is assigned to a private field.';
6667
} else if (field.getter == null) {
6768
assert(field.setter != null);
6869
unavailableReasons[field.name] =
6970
'Setter-only properties are not supported.';
7071
log.warning('Setters are ignored: ${element.name}.${field.name}');
71-
} else if (jsonKeyFor(field).ignore) {
72+
} else if (jsonKey.explicitNoFromJson) {
7273
unavailableReasons[field.name] =
73-
'It is assigned to an ignored field.';
74+
'It is assigned to a field not meant to be used in fromJson.';
7475
} else {
7576
assert(!map.containsKey(field.name));
7677
map[field.name] = field;
@@ -85,28 +86,47 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
8586
final createResult = createFactory(accessibleFields, unavailableReasons);
8687
yield createResult.output;
8788

88-
accessibleFieldSet = accessibleFields.entries
89+
final fieldsToUse = accessibleFields.entries
8990
.where((e) => createResult.usedFields.contains(e.key))
9091
.map((e) => e.value)
91-
.toSet();
92+
.toList();
93+
94+
// Need to add candidates BACK even if they are not used in the factory if
95+
// they are forced to be used for toJSON
96+
for (var candidate in sortedFields.where((element) =>
97+
jsonKeyFor(element).explicitYesToJson &&
98+
!fieldsToUse.contains(element))) {
99+
fieldsToUse.add(candidate);
100+
}
101+
102+
// Need the fields to maintain the original source ordering
103+
fieldsToUse.sort(
104+
(a, b) => sortedFields.indexOf(a).compareTo(sortedFields.indexOf(b)));
105+
106+
accessibleFieldSet = fieldsToUse.toSet();
92107
}
93108

94-
// Check for duplicate JSON keys due to colliding annotations.
95-
// We do this now, since we have a final field list after any pruning done
96-
// by `_writeCtor`.
97-
accessibleFieldSet.fold(
98-
<String>{},
99-
(Set<String> set, fe) {
100-
final jsonKey = nameAccess(fe);
101-
if (!set.add(jsonKey)) {
102-
throw InvalidGenerationSourceError(
103-
'More than one field has the JSON key for name "$jsonKey".',
104-
element: fe,
105-
);
106-
}
107-
return set;
108-
},
109-
);
109+
accessibleFieldSet
110+
..removeWhere(
111+
(element) => jsonKeyFor(element).explicitNoToJson,
112+
)
113+
114+
// Check for duplicate JSON keys due to colliding annotations.
115+
// We do this now, since we have a final field list after any pruning done
116+
// by `_writeCtor`.
117+
..fold(
118+
<String>{},
119+
(Set<String> set, fe) {
120+
final jsonKey = nameAccess(fe);
121+
if (!set.add(jsonKey)) {
122+
throw InvalidGenerationSourceError(
123+
'More than one field has the JSON key for name "$jsonKey".',
124+
element: fe,
125+
);
126+
}
127+
return set;
128+
},
129+
);
110130

111131
if (config.createFieldMap) {
112132
yield createFieldMap(accessibleFieldSet);
@@ -123,3 +143,13 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
123143
yield* _addedMembers;
124144
}
125145
}
146+
147+
extension on KeyConfig {
148+
bool get explicitYesFromJson => includeFromJson == true;
149+
150+
bool get explicitNoFromJson => includeFromJson == false;
151+
152+
bool get explicitYesToJson => includeToJson == true;
153+
154+
bool get explicitNoToJson => includeFromJson == false;
155+
}

json_serializable/lib/src/json_key_utils.dart

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
3434
classAnnotation,
3535
element,
3636
defaultValue: ctorParamDefault,
37-
ignore: classAnnotation.ignoreUnannotated,
37+
includeFromJson: classAnnotation.ignoreUnannotated ? false : null,
38+
includeToJson: classAnnotation.ignoreUnannotated ? false : null,
3839
);
3940
}
4041

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

240+
final ignore = obj.read('ignore').literalValue as bool?;
241+
var includeFromJson = obj.read('includeFromJson').literalValue as bool?;
242+
var includeToJson = obj.read('includeToJson').literalValue as bool?;
243+
244+
if (ignore != null) {
245+
if (includeFromJson != null) {
246+
throwUnsupported(
247+
element,
248+
'Cannot use both `ignore` and `includeFromJson` on the same field. '
249+
'Since `ignore` is deprecated, you should only use `includeFromJson`.',
250+
);
251+
}
252+
if (includeToJson != null) {
253+
throwUnsupported(
254+
element,
255+
'Cannot use both `ignore` and `includeToJson` on the same field. '
256+
'Since `ignore` is deprecated, you should only use `includeToJson`.',
257+
);
258+
}
259+
assert(includeFromJson == null && includeToJson == null);
260+
includeToJson = includeFromJson = !ignore;
261+
}
262+
239263
return _populateJsonKey(
240264
classAnnotation,
241265
element,
242266
defaultValue: defaultValue ?? ctorParamDefault,
243267
disallowNullValue: obj.read('disallowNullValue').literalValue as bool?,
244-
ignore: obj.read('ignore').literalValue as bool?,
245268
includeIfNull: obj.read('includeIfNull').literalValue as bool?,
246269
name: obj.read('name').literalValue as String?,
247270
readValueFunctionName: readValueFunctionName,
248271
required: obj.read('required').literalValue as bool?,
249272
unknownEnumValue:
250273
createAnnotationValue('unknownEnumValue', mustBeEnum: true),
274+
includeToJson: includeToJson,
275+
includeFromJson: includeFromJson,
251276
);
252277
}
253278

@@ -256,12 +281,13 @@ KeyConfig _populateJsonKey(
256281
FieldElement element, {
257282
required String? defaultValue,
258283
bool? disallowNullValue,
259-
bool? ignore,
260284
bool? includeIfNull,
261285
String? name,
262286
String? readValueFunctionName,
263287
bool? required,
264288
String? unknownEnumValue,
289+
bool? includeToJson,
290+
bool? includeFromJson,
265291
}) {
266292
if (disallowNullValue == true) {
267293
if (includeIfNull == true) {
@@ -275,13 +301,14 @@ KeyConfig _populateJsonKey(
275301
return KeyConfig(
276302
defaultValue: defaultValue,
277303
disallowNullValue: disallowNullValue ?? false,
278-
ignore: ignore ?? false,
279304
includeIfNull: _includeIfNull(
280305
includeIfNull, disallowNullValue, classAnnotation.includeIfNull),
281306
name: name ?? encodedFieldName(classAnnotation.fieldRename, element.name),
282307
readValueFunctionName: readValueFunctionName,
283308
required: required ?? false,
284309
unknownEnumValue: unknownEnumValue,
310+
includeFromJson: includeFromJson,
311+
includeToJson: includeToJson,
285312
);
286313
}
287314

json_serializable/lib/src/type_helpers/config_types.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ class KeyConfig {
1111

1212
final bool disallowNullValue;
1313

14-
final bool ignore;
14+
final bool? includeFromJson;
1515

1616
final bool includeIfNull;
1717

18+
final bool? includeToJson;
19+
1820
final String name;
1921

2022
final bool required;
@@ -26,8 +28,9 @@ class KeyConfig {
2628
KeyConfig({
2729
required this.defaultValue,
2830
required this.disallowNullValue,
29-
required this.ignore,
31+
required this.includeFromJson,
3032
required this.includeIfNull,
33+
required this.includeToJson,
3134
required this.name,
3235
required this.readValueFunctionName,
3336
required this.required,

0 commit comments

Comments
 (0)