@@ -114,29 +114,45 @@ macro class FromJson implements ConstructorDefinitionMacro {
114
114
115
115
var fields = await builder.fieldsOf (clazz);
116
116
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 ) ...[
136
125
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);
140
156
}
141
157
142
158
Future <void > _checkValidFromJson (ConstructorDeclaration constructor,
@@ -257,9 +273,10 @@ macro class FromJson implements ConstructorDefinitionMacro {
257
273
}
258
274
}
259
275
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 {
263
280
ConstructorMetadataAnnotation ? jsonKey;
264
281
for (var annotation in metadata) {
265
282
if (annotation is ! ConstructorMetadataAnnotation ) continue ;
@@ -277,8 +294,55 @@ extension _ on FieldDeclaration {
277
294
jsonKey = annotation;
278
295
}
279
296
}
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
+ );
282
346
}
283
347
}
284
348
@@ -391,21 +455,47 @@ macro class ToJson implements MethodDefinitionMacro {
391
455
}
392
456
393
457
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));
409
499
}
410
500
411
501
Future <bool > _checkValidToJson (MethodDeclaration method,
@@ -509,33 +599,39 @@ final class _ToJsonData {
509
599
final StaticType jsonMapType;
510
600
final StaticType listType;
511
601
final StaticType mapType;
602
+ final Identifier nullIdentifier;
512
603
final NamedTypeAnnotationCode objectCode;
513
604
final StaticType objectType;
514
605
final StaticType setType;
606
+ final NamedTypeAnnotationCode stringCode;
515
607
516
608
_ToJsonData ({
517
609
required this .jsonMapType,
518
610
required this .listType,
519
611
required this .mapType,
612
+ required this .nullIdentifier,
520
613
required this .objectCode,
521
614
required this .objectType,
522
615
required this .setType,
616
+ required this .stringCode,
523
617
});
524
618
525
619
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 ([
527
621
builder.resolveIdentifier (_dartCore, 'List' ),
528
622
builder.resolveIdentifier (_dartCore, 'Map' ),
623
+ builder.resolveIdentifier (_dartCore, 'Null' ),
529
624
builder.resolveIdentifier (_dartCore, 'Object' ),
530
625
builder.resolveIdentifier (_dartCore, 'Set' ),
531
626
builder.resolveIdentifier (_dartCore, 'String' ),
532
627
]);
533
628
var objectCode = NamedTypeAnnotationCode (name: object);
629
+ var stringCode = NamedTypeAnnotationCode (name: string);
534
630
var nullableObjectCode = objectCode.asNullable;
535
631
var [jsonMapType, listType, mapType, objectType, setType] =
536
632
await Future .wait ([
537
633
builder.resolve (NamedTypeAnnotationCode (name: map, typeArguments: [
538
- NamedTypeAnnotationCode (name : string) ,
634
+ stringCode ,
539
635
nullableObjectCode,
540
636
])),
541
637
builder.resolve (NamedTypeAnnotationCode (
@@ -551,9 +647,11 @@ final class _ToJsonData {
551
647
jsonMapType: jsonMapType,
552
648
listType: listType,
553
649
mapType: mapType,
650
+ nullIdentifier: nullIdentifier,
554
651
objectCode: objectCode,
555
652
objectType: objectType,
556
653
setType: setType,
654
+ stringCode: stringCode,
557
655
);
558
656
}
559
657
}
0 commit comments