@@ -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}
0 commit comments