Skip to content

Commit d2fe514

Browse files
authored
Allow customizable "reading" of JSON maps (#1045)
Allows aliasing values to different names or computing a single field value from any aspect of the source JSON map Fixes #144 Fixes #888
1 parent a36f266 commit d2fe514

24 files changed

+208
-42
lines changed

_test_yaml/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dev_dependencies:
88
build_runner: ^2.0.0
99
build_verify: ^2.0.0
1010
checked_yaml: any
11-
json_annotation: ^4.3.0
11+
json_annotation: ^4.4.0
1212
json_serializable: any
1313
test: ^1.6.0
1414
yaml: ^3.0.0

json_annotation/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
## 4.3.1-dev
1+
## 4.4.0-dev
22

3+
- Added `JsonKey.readValue`.
4+
- Non-breaking updates to `checkedCreate` and `checkedConvert` to support
5+
`JsonKey.readValue`.
36
- Improved `toString` in included exceptions.
47

58
## 4.3.0

json_annotation/lib/src/checked_helpers.dart

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,22 @@ typedef _CastFunction<R> = R Function(Object?);
1313
T $checkedCreate<T>(
1414
String className,
1515
Map map,
16-
T Function(S Function<S>(String, _CastFunction<S>) converter) constructor, {
16+
T Function(
17+
S Function<S>(
18+
String,
19+
_CastFunction<S>, {
20+
Object? Function(Map, String)? readValue,
21+
}),
22+
)
23+
constructor, {
1724
Map<String, String> fieldKeyMap = const {},
1825
}) {
19-
Q _checkedConvert<Q>(String key, _CastFunction<Q> convertFunction) =>
20-
$checkedConvert<Q>(map, key, convertFunction);
26+
Q _checkedConvert<Q>(
27+
String key,
28+
_CastFunction<Q> convertFunction, {
29+
Object? Function(Map, String)? readValue,
30+
}) =>
31+
$checkedConvert<Q>(map, key, convertFunction, readValue: readValue);
2132

2233
return $checkedNew(
2334
className,
@@ -69,9 +80,14 @@ T $checkedNew<T>(
6980
/// `JsonSerializableGenerator.checked` is `true`.
7081
///
7182
/// Should not be used directly.
72-
T $checkedConvert<T>(Map map, String key, T Function(dynamic) castFunc) {
83+
T $checkedConvert<T>(
84+
Map map,
85+
String key,
86+
T Function(dynamic) castFunc, {
87+
Object? Function(Map, String)? readValue,
88+
}) {
7389
try {
74-
return castFunc(map[key]);
90+
return castFunc(readValue == null ? map[key] : readValue(map, key));
7591
} on CheckedFromJsonException {
7692
rethrow;
7793
} catch (error, stack) {

json_annotation/lib/src/json_key.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ class JsonKey {
6767
/// If `null`, the field name is used.
6868
final String? name;
6969

70+
/// Specialize how a value is read from the source JSON map.
71+
///
72+
/// Typically, the value corresponding to a given key is read directly from
73+
/// the JSON map using `map[key]`. At times it's convenient to customize this
74+
/// behavior to support alternative names or to support logic that requires
75+
/// accessing multiple values at once.
76+
///
77+
/// The provided, the [Function] must be a top-level or static within the
78+
/// using class.
79+
///
80+
/// Note: using this feature does not change any of the subsequent decoding
81+
/// logic for the field. For instance, if the field is of type [DateTime] we
82+
/// expect the function provided here to return a [String].
83+
final Object? Function(Map, String)? readValue;
84+
7085
/// When `true`, generated code for `fromJson` will verify that the source
7186
/// JSON map contains the associated key.
7287
///
@@ -108,6 +123,7 @@ class JsonKey {
108123
this.ignore,
109124
this.includeIfNull,
110125
this.name,
126+
this.readValue,
111127
this.required,
112128
this.toJson,
113129
this.unknownEnumValue,

json_serializable/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 6.1.0-dev
2+
3+
- Support `JsonKey.readValue` to allow customized reading of values from source
4+
JSON map objects.
5+
- Require `json_annotation` `'>=4.4.0 <4.5.0'`.
6+
17
## 6.0.1
28

39
- Don't require `json_annotation` in `dependencies` if it's just used in tests.

json_serializable/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,14 @@ targets:
199199
[`Enum`]: https://api.dart.dev/stable/dart-core/Enum-class.html
200200
[`int`]: https://api.dart.dev/stable/dart-core/int-class.html
201201
[`Iterable`]: https://api.dart.dev/stable/dart-core/Iterable-class.html
202-
[`JsonConverter`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonConverter-class.html
203-
[`JsonEnum`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonEnum-class.html
204-
[`JsonKey.fromJson`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonKey/fromJson.html
205-
[`JsonKey.toJson`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonKey/toJson.html
206-
[`JsonKey`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonKey-class.html
207-
[`JsonLiteral`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonLiteral-class.html
208-
[`JsonSerializable`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonSerializable-class.html
209-
[`JsonValue`]: https://pub.dev/documentation/json_annotation/4.3.0/json_annotation/JsonValue-class.html
202+
[`JsonConverter`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
203+
[`JsonEnum`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonEnum-class.html
204+
[`JsonKey.fromJson`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
205+
[`JsonKey.toJson`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
206+
[`JsonKey`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey-class.html
207+
[`JsonLiteral`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonLiteral-class.html
208+
[`JsonSerializable`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonSerializable-class.html
209+
[`JsonValue`]: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonValue-class.html
210210
[`List`]: https://api.dart.dev/stable/dart-core/List-class.html
211211
[`Map`]: https://api.dart.dev/stable/dart-core/Map-class.html
212212
[`num`]: https://api.dart.dev/stable/dart-core/num-class.html

json_serializable/lib/src/check_dependencies.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'package:pubspec_parse/pubspec_parse.dart';
1010

1111
const _productionDirectories = {'lib', 'bin'};
1212
const _annotationPkgName = 'json_annotation';
13-
final requiredJsonAnnotationMinVersion = Version.parse('4.3.0');
13+
final requiredJsonAnnotationMinVersion = Version.parse('4.4.0');
1414

1515
Future<void> pubspecHasRightVersion(BuildStep buildStep) async {
1616
final segments = buildStep.inputId.pathSegments;

json_serializable/lib/src/decode_helper.dart

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,24 @@ abstract class DecodeHelper implements HelperCore {
8686
..write('''
8787
final val = ${data.content};''');
8888

89-
for (final field in data.fieldsToSet) {
89+
for (final fieldName in data.fieldsToSet) {
9090
sectionBuffer.writeln();
91-
final safeName = safeNameAccess(accessibleFields[field]!);
91+
final fieldValue = accessibleFields[fieldName]!;
92+
final safeName = safeNameAccess(fieldValue);
9293
sectionBuffer
9394
..write('''
9495
\$checkedConvert($safeName, (v) => ''')
95-
..write('val.$field = ')
96-
..write(_deserializeForField(accessibleFields[field]!,
97-
checkedProperty: true))
98-
..write(');');
96+
..write('val.$fieldName = ')
97+
..write(
98+
_deserializeForField(fieldValue, checkedProperty: true),
99+
);
100+
101+
final readValueFunc = jsonKeyFor(fieldValue).readValueFunctionName;
102+
if (readValueFunc != null) {
103+
sectionBuffer.writeln(',readValue: $readValueFunc,');
104+
}
105+
106+
sectionBuffer.write(');');
99107
}
100108

101109
sectionBuffer.write('''\n return val;
@@ -182,6 +190,8 @@ abstract class DecodeHelper implements HelperCore {
182190
}
183191
}
184192

193+
/// If [checkedProperty] is `true`, we're using this function to write to a
194+
/// setter.
185195
String _deserializeForField(
186196
FieldElement field, {
187197
ParameterElement? ctorParam,
@@ -192,6 +202,7 @@ abstract class DecodeHelper implements HelperCore {
192202
final contextHelper = getHelperContext(field);
193203
final jsonKey = jsonKeyFor(field);
194204
final defaultValue = jsonKey.defaultValue;
205+
final readValueFunc = jsonKey.readValueFunctionName;
195206

196207
String deserialize(String expression) => contextHelper
197208
.deserialize(
@@ -206,14 +217,21 @@ abstract class DecodeHelper implements HelperCore {
206217
if (config.checked) {
207218
value = deserialize('v');
208219
if (!checkedProperty) {
209-
value = '\$checkedConvert($jsonKeyName, (v) => $value)';
220+
final readValueBit =
221+
readValueFunc == null ? '' : ',readValue: $readValueFunc,';
222+
value = '\$checkedConvert($jsonKeyName, (v) => $value$readValueBit)';
210223
}
211224
} else {
212225
assert(
213226
!checkedProperty,
214227
'should only be true if `_generator.checked` is true.',
215228
);
216-
value = deserialize('json[$jsonKeyName]');
229+
230+
value = deserialize(
231+
readValueFunc == null
232+
? 'json[$jsonKeyName]'
233+
: '$readValueFunc(json, $jsonKeyName)',
234+
);
217235
}
218236
} on UnsupportedTypeError catch (e) // ignore: avoid_catching_errors
219237
{

json_serializable/lib/src/json_key_utils.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
209209
}
210210
}
211211

212+
String? readValueFunctionName;
213+
final readValue = obj.read('readValue');
214+
if (!readValue.isNull) {
215+
final objValue = readValue.objectValue.toFunctionValue()!;
216+
readValueFunctionName = objValue.name;
217+
}
218+
212219
return _populateJsonKey(
213220
classAnnotation,
214221
element,
@@ -217,6 +224,7 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
217224
ignore: obj.read('ignore').literalValue as bool?,
218225
includeIfNull: obj.read('includeIfNull').literalValue as bool?,
219226
name: obj.read('name').literalValue as String?,
227+
readValueFunctionName: readValueFunctionName,
220228
required: obj.read('required').literalValue as bool?,
221229
unknownEnumValue: _annotationValue('unknownEnumValue', mustBeEnum: true),
222230
);
@@ -230,6 +238,7 @@ KeyConfig _populateJsonKey(
230238
bool? ignore,
231239
bool? includeIfNull,
232240
String? name,
241+
String? readValueFunctionName,
233242
bool? required,
234243
String? unknownEnumValue,
235244
}) {
@@ -249,6 +258,7 @@ KeyConfig _populateJsonKey(
249258
includeIfNull: _includeIfNull(
250259
includeIfNull, disallowNullValue, classAnnotation.includeIfNull),
251260
name: name ?? encodedFieldName(classAnnotation.fieldRename, element.name),
261+
readValueFunctionName: readValueFunctionName,
252262
required: required ?? false,
253263
unknownEnumValue: unknownEnumValue,
254264
);

json_serializable/lib/src/type_helpers/config_types.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ class KeyConfig {
2020

2121
final String? unknownEnumValue;
2222

23+
final String? readValueFunctionName;
24+
2325
KeyConfig({
2426
required this.defaultValue,
2527
required this.disallowNullValue,
2628
required this.ignore,
2729
required this.includeIfNull,
2830
required this.name,
31+
required this.readValueFunctionName,
2932
required this.required,
3033
required this.unknownEnumValue,
3134
});

0 commit comments

Comments
 (0)