Skip to content

Commit ac10f01

Browse files
jakemac53Commit Queue
authored and
Commit Queue
committed
add default value configuration as well as includeIfNull configuration
Change-Id: Ia5e8e3b6539047087dc2cf79f9375fe4ee58d2fe Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/351280 Reviewed-by: Morgan :) <[email protected]> Commit-Queue: Jake Macdonald <[email protected]> Auto-Submit: Jake Macdonald <[email protected]>
1 parent a1b4f29 commit ac10f01

File tree

3 files changed

+161
-49
lines changed

3 files changed

+161
-49
lines changed

tests/language/macros/json/json_key.dart

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,14 @@ class JsonKey {
1313
/// If `null`, the field name is used.
1414
final String? name;
1515

16-
const JsonKey({this.name});
16+
/// The value to use if the source JSON does not contain this key.
17+
///
18+
/// If the value is explicitly null in the JSON, it will still be retained.
19+
final Object? defaultValue;
20+
21+
/// Whether or not to include this field in the serialized form, even if it
22+
/// is `null`.
23+
final bool includeIfNull;
24+
25+
const JsonKey({this.name, this.defaultValue, this.includeIfNull = false});
1726
}

tests/language/macros/json/json_serializable.dart

+142-44
Original file line numberDiff line numberDiff line change
@@ -114,29 +114,45 @@ macro class FromJson implements ConstructorDefinitionMacro {
114114

115115
var fields = await builder.fieldsOf(clazz);
116116
var jsonParam = constructor.positionalParameters.single.identifier;
117-
builder.augment(initializers: [
118-
for (var field in fields)
119-
RawCode.fromParts([
120-
field.identifier,
121-
' = ',
122-
await _convertTypeFromJson(
123-
field.type,
124-
RawCode.fromParts([
125-
jsonParam,
126-
'[',
127-
await field._jsonKeyName(builder),
128-
']',
129-
]),
130-
builder,
131-
fromJsonData),
132-
]),
133-
if (superclassHasFromJson)
134-
RawCode.fromParts([
135-
'super.fromJson(',
117+
var initializers = <Code>[];
118+
for (var field in fields) {
119+
var config = await field.readConfig(builder);
120+
var defaultValue = config.defaultValue;
121+
initializers.add(RawCode.fromParts([
122+
field.identifier,
123+
' = ',
124+
if (defaultValue != null) ...[
136125
jsonParam,
137-
')',
138-
]),
139-
]);
126+
'.containsKey(',
127+
config.key,
128+
') ? ',
129+
],
130+
await _convertTypeFromJson(
131+
field.type,
132+
RawCode.fromParts([
133+
jsonParam,
134+
'[',
135+
config.key,
136+
']',
137+
]),
138+
builder,
139+
fromJsonData),
140+
if (defaultValue != null) ...[
141+
' : ',
142+
defaultValue,
143+
],
144+
]));
145+
}
146+
147+
if (superclassHasFromJson) {
148+
initializers.add(RawCode.fromParts([
149+
'super.fromJson(',
150+
jsonParam,
151+
')',
152+
]));
153+
}
154+
155+
builder.augment(initializers: initializers);
140156
}
141157

142158
Future<void> _checkValidFromJson(ConstructorDeclaration constructor,
@@ -257,9 +273,10 @@ macro class FromJson implements ConstructorDefinitionMacro {
257273
}
258274
}
259275

260-
extension _ on FieldDeclaration {
261-
// TODO: Support `IdentifierMetadataAnnotation`s once we can do constant eval.
262-
Future<Code> _jsonKeyName(DefinitionBuilder builder) async {
276+
extension on FieldDeclaration {
277+
/// Returns the configuration data for this field, reading it from the
278+
/// `JsonKey` annotation if present, and otherwise using defaults.
279+
Future<_FieldConfig> readConfig(DefinitionBuilder builder) async {
263280
ConstructorMetadataAnnotation? jsonKey;
264281
for (var annotation in metadata) {
265282
if (annotation is! ConstructorMetadataAnnotation) continue;
@@ -277,8 +294,55 @@ extension _ on FieldDeclaration {
277294
jsonKey = annotation;
278295
}
279296
}
280-
return jsonKey?.namedArguments['name'] ??
281-
RawCode.fromString('\'${identifier.name}\'');
297+
return _FieldConfig(this, jsonKey);
298+
}
299+
}
300+
301+
final class _FieldConfig {
302+
final Code? defaultValue;
303+
304+
final Code key;
305+
306+
final bool includeIfNull;
307+
308+
_FieldConfig._({
309+
required this.defaultValue,
310+
required this.includeIfNull,
311+
required this.key,
312+
});
313+
314+
factory _FieldConfig(
315+
FieldDeclaration field, ConstructorMetadataAnnotation? jsonKey) {
316+
bool? includeIfNull;
317+
var includeIfNullArg = jsonKey?.namedArguments['includeIfNull'];
318+
if (includeIfNullArg != null) {
319+
if (!field.type.isNullable) {
320+
throw DiagnosticException(Diagnostic(
321+
DiagnosticMessage(
322+
'`includeIfNull` cannot be used for non-nullable fields',
323+
target: jsonKey!.asDiagnosticTarget),
324+
Severity.error));
325+
}
326+
// TODO: Use constant eval to do this better.
327+
var argString = includeIfNullArg.debugString;
328+
includeIfNull = switch (argString) {
329+
'false' => false,
330+
'true' => true,
331+
_ => throw DiagnosticException(Diagnostic(
332+
DiagnosticMessage(
333+
'Only `true` or `false` literals are allowed for '
334+
'`includeIfNull` arguments.',
335+
target: jsonKey!.asDiagnosticTarget),
336+
Severity.error)),
337+
};
338+
}
339+
340+
return _FieldConfig._(
341+
defaultValue: jsonKey?.namedArguments['defaultValue'],
342+
includeIfNull: includeIfNull ?? false,
343+
key: jsonKey?.namedArguments['name'] ??
344+
RawCode.fromString('\'${field.identifier.name}\''),
345+
);
282346
}
283347
}
284348

@@ -391,21 +455,47 @@ macro class ToJson implements MethodDefinitionMacro {
391455
}
392456

393457
var fields = await builder.fieldsOf(clazz);
394-
builder.augment(FunctionBodyCode.fromParts([
395-
' => {',
396-
// TODO: Avoid the extra copying here.
397-
if (superclassHasToJson) '\n ...super.toJson(),',
398-
for (var field in fields)
399-
RawCode.fromParts([
400-
'\n ',
401-
await field._jsonKeyName(builder),
402-
': ',
403-
await _convertTypeToJson(field.type,
404-
RawCode.fromParts([field.identifier]), builder, toJsonData),
405-
',',
406-
]),
407-
'\n };',
408-
]));
458+
var parts = <Object>[
459+
'{\n var json = ',
460+
if (superclassHasToJson)
461+
'super.toJson()'
462+
else ...[
463+
'<',
464+
toJsonData.stringCode,
465+
', ',
466+
toJsonData.objectCode.asNullable,
467+
'>{}',
468+
],
469+
';\n '
470+
];
471+
for (var field in fields) {
472+
var config = await field.readConfig(builder);
473+
var doNullCheck = !config.includeIfNull && field.type.isNullable;
474+
if (doNullCheck) {
475+
// TODO: Compare == `null` instead, once we can resolve `null`.
476+
parts.addAll([
477+
'if (',
478+
field.identifier,
479+
' is! ',
480+
toJsonData.nullIdentifier,
481+
') {\n ',
482+
]);
483+
}
484+
parts.addAll([
485+
'json[',
486+
config.key,
487+
'] = ',
488+
await _convertTypeToJson(field.type,
489+
RawCode.fromParts([field.identifier]), builder, toJsonData),
490+
';\n',
491+
]);
492+
if (doNullCheck) {
493+
parts.add(' }\n');
494+
}
495+
}
496+
parts.add(' return json;\n }');
497+
498+
builder.augment(FunctionBodyCode.fromParts(parts));
409499
}
410500

411501
Future<bool> _checkValidToJson(MethodDeclaration method,
@@ -509,33 +599,39 @@ final class _ToJsonData {
509599
final StaticType jsonMapType;
510600
final StaticType listType;
511601
final StaticType mapType;
602+
final Identifier nullIdentifier;
512603
final NamedTypeAnnotationCode objectCode;
513604
final StaticType objectType;
514605
final StaticType setType;
606+
final NamedTypeAnnotationCode stringCode;
515607

516608
_ToJsonData({
517609
required this.jsonMapType,
518610
required this.listType,
519611
required this.mapType,
612+
required this.nullIdentifier,
520613
required this.objectCode,
521614
required this.objectType,
522615
required this.setType,
616+
required this.stringCode,
523617
});
524618

525619
static Future<_ToJsonData> build(FunctionDefinitionBuilder builder) async {
526-
var [list, map, object, set, string] = await Future.wait([
620+
var [list, map, nullIdentifier, object, set, string] = await Future.wait([
527621
builder.resolveIdentifier(_dartCore, 'List'),
528622
builder.resolveIdentifier(_dartCore, 'Map'),
623+
builder.resolveIdentifier(_dartCore, 'Null'),
529624
builder.resolveIdentifier(_dartCore, 'Object'),
530625
builder.resolveIdentifier(_dartCore, 'Set'),
531626
builder.resolveIdentifier(_dartCore, 'String'),
532627
]);
533628
var objectCode = NamedTypeAnnotationCode(name: object);
629+
var stringCode = NamedTypeAnnotationCode(name: string);
534630
var nullableObjectCode = objectCode.asNullable;
535631
var [jsonMapType, listType, mapType, objectType, setType] =
536632
await Future.wait([
537633
builder.resolve(NamedTypeAnnotationCode(name: map, typeArguments: [
538-
NamedTypeAnnotationCode(name: string),
634+
stringCode,
539635
nullableObjectCode,
540636
])),
541637
builder.resolve(NamedTypeAnnotationCode(
@@ -551,9 +647,11 @@ final class _ToJsonData {
551647
jsonMapType: jsonMapType,
552648
listType: listType,
553649
mapType: mapType,
650+
nullIdentifier: nullIdentifier,
554651
objectCode: objectCode,
555652
objectType: objectType,
556653
setType: setType,
654+
stringCode: stringCode,
557655
);
558656
}
559657
}

tests/language/macros/json/json_serializable_test.dart

+9-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ void main() {
1515
'name': 'Roger',
1616
'friends': [
1717
{
18-
'age': 7,
1918
'name': 'Felix',
20-
'friends': [],
2119
}
2220
],
2321
};
@@ -26,9 +24,12 @@ void main() {
2624
Expect.equals(roger.name, 'Roger');
2725
Expect.equals(roger.friends.length, 1);
2826
var felix = roger.friends.single;
29-
Expect.equals(felix.age, 7);
27+
Expect.equals(felix.age, null);
3028
Expect.equals(felix.name, 'Felix');
3129
Expect.equals(felix.friends.isEmpty, true);
30+
31+
// When serializing back, we expect the default value
32+
(rogerJson['friends'] as dynamic)[0]['friends'] = [];
3233
Expect.deepEquals(roger.toJson(), rogerJson);
3334

3435
var rogerAccountJson = {
@@ -52,8 +53,12 @@ void main() {
5253

5354
@JsonSerializable()
5455
class User {
55-
final int age;
56+
@JsonKey(includeIfNull: false)
57+
final int? age;
58+
5659
final String name;
60+
61+
@JsonKey(defaultValue: <User>[])
5762
final List<User> friends;
5863
}
5964

0 commit comments

Comments
 (0)