Json_serializable.dart: Propose decodeIfNull Option

Created on 8 Feb 2021  Â·  11Comments  Â·  Source: google/json_serializable.dart

I have an API client that I am migrating to null safety. Some values from this API can be null and I would like a way to ignore those values. I cannot specify a default in the JsonKey attribute because many of the classes have DateTime values. The DateTime type does not have any constants for min/max date like other frameworks and no constant constructor exists.

It would be beneficial to have a decodeIfNull property added to the JsonSerializable and JsonKey attributes.

It cannot work on classes that rely on deserialization using the constructor. Constructor arguments cannot be conditionally included at runtime so only classes that allow field/property setters would be included.

The decodeIfNull option can be specified in the build.yaml, the JsonSerializable attribute, and the JsonKey attribute. The value override behavior would be the same as the other options shared between the JsonSerializable and JsonKey attributes.

The generator should create runtime null checks for each field/property set operation. If the value to be deserialized is null, the field/property set operation is skipped. This will allow classes to directly control their default values without any reliance on the JsonKey attribute. The JsonKey behavior would remain unchanged if the value to be deserialized is not null.

All 11 comments

What about @JsonSerializable attribute includeIfNull? Isn't it what you're trying to achieve?

Regarding min/max DateTime, you can pass min/max DateTime as sting values -infinity and infinity and process these values in your own Converter.
I use this

@JsonSerializable(includeIfNull: false, createToJson: false)
class Model {
  Model({required this.dateTime, required this.name});
  factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);

  String name;
  @DateTimeConverter()
  DateTime dateTime;
}

class DateTimeConverter implements JsonConverter<DateTime, Object> {
  const DateTimeConverter();

  // DateTimes can represent time values that are at a distance of at most 100,000,000
  // days from epoch (1970-01-01 UTC): -271821-04-20 to 275760-09-13.
  static DateTime minDateTime = DateTime.utc(-271821, 04, 20);
  static DateTime maxDateTime = DateTime.utc(275760, 09, 13);

  @override
  DateTime fromJson(Object json) {
    if (json is DateTime) {
      return json;
    } else if (json is String) {
      if (json == '-infinity') {
        return minDateTime;
      } else if (json == 'infinity') {
        return maxDateTime;
      } else {
        return DateTime.parse(json);
      }
    } else {
      throw Exception('Invalid input for DateTime');
    }
  }

  @override
  String toJson(DateTime dateTime) {
    if (dateTime == minDateTime) {
      return '-infinity';
    } else if (dateTime == maxDateTime) {
      return 'infinity';
    } else {
      return dateTime.toIso8601String();
    }
  }
}

According to the documentation and my testing, the includeIfNull option only applies to serialized output. I am looking for a mechanism to ignore null during deserialization. I do not want to make the property value nullable because I am updating my client to null safety. I want as few nullable values as possible and work with sensible defaults instead.

The API I am calling is 3rd party and I have no control over what it returns. I would like to specify min and max dates to be used in place of null with default values for the class fields. Without the option to ignore null during deserialization, the serializer tries to convert null to a non-nullable value and fails. DateTime does not have any constants so no value can be provided in the attributes.

I will implement your solution as it is a very sound alternative to a native option. I think a decodeIfNull option would still be beneficial so developers to not have to write converters for every value that does not have a constant available. The class can be structured with default values for each field without the serializer trying to change those values if the input value is null.

I like recommendation from @edlman

I want to REMOVE things from JsonKey. We have too many knobs now. I'm trying to avoid making the implementation more complex.

I would like to second @JSanford42

I am moving a client to null safety as well and would prefer sensible defaults for all fields instead of just setting them all to null. This is a huge JSON document with a couple of thousand unique keys.

Ideally "includeIfNull" also applied to decoding, but that would be a breaking change.

I think that code like

class Data {
     String name = ""
}

is better than

class Data {
    @JsonKey(defaultValue: "")
    String name = ""
}

As it adds heaps of boilerplate and goes against DRY principles.

Let me spend some time tomorrow to see if I can crack the default value out
of the parameter definition in the constructor. That would probably be what
everyone wants anyway

On Thu, Mar 25, 2021, 21:37 Ryan Knell @.*> wrote:

I would like to second @JSanford42 https://github.com/JSanford42

I am moving a client to null safety as well and would prefer sensible
defaults for all fields instead of just setting them all to null. This is a
huge JSON document with a couple of thousand unique keys.

Ideally "includeIfNull" also applied to decoding, but that would be a
breaking change.

I think that code like

class Data {
String name = ""
}

is better than

class Data {
@JsonKey(defaultValue: "")
String name = ""
}

As it adds heaps of boilerplate and goes against DRY principles.

—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
https://github.com/google/json_serializable.dart/issues/796#issuecomment-807929271,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAAEFCTS3OF2O7U44DBCX4DTFQFPRANCNFSM4XJQSSSQ
.

Oh wow!

I started rolling my own solution for this but just found the commits. Thanks so much for taking this on board

@kevmoo Thanks for taking the time to implement the use of the constructor default values.

There is still an issue with classes that cannot provide a default value in the constructor.

// Null safety requires all non-nullable class members be initialized.
// This example uses constructor initialization to illustrate the issue.

// No constants or constant constructors exist for DateTime.
// The Dart team has provided reasons for this in the Dart repo.
final DateTime defaultDate = DateTime(1970, 1, 1);

@JsonSerializable()
class Child {
  // @DateTimeConverter()
  DateTime date;
  int id;

  // Default value for date cannot be specified in constructor argument because it is not a constant.
  Child({this.id = 0}) : date = defaultDate;
}

@JsonSerializable()
class Parent {
  // @ChildConverter()
  Child child;

  // Inability to specify a default value determined by inability of child to provide a constant constructor.
  Parent() : child = Child();
}

Two converters will be required for this example. A converter for DateTime and a converter for the Child class. The use of the new converters forces the use of field setters for those class members because the converters cannot be called in the constructor. The required use of a field setter prevents the fields from being final and, in turn, prevents the class from defining a constant constructor.

Wait @JSanford42 – why can't you use converters w/ the constructor? I'm confused.

The converter takes in Map<String, dynamic>?. If it's null, return Child() otherwise return Child.fromJson(json)

I apologize for not being clear. The converters cannot be called to assign the default value of an argument in a constant constructor. I was not referring to the constructor body.

@JSanford42 – this works fine, though.

final _defaultDate = DateTime(1970, 1, 1);

@JsonSerializable()
class Child {
  // @DateTimeConverter()
  final DateTime date;
  final int id;

  Child({
    this.id = 0,
    DateTime? date,
  }) : date = date ?? _defaultDate;
}

I have updated my client to use the constructor method in your example. I am not thrilled about having to create monolithic constructors to be able to set default values that do not have constants but I will use this method from now on.

I understand the desire to reduce the amount of options with JsonKey and JsonSerializable. I find it somewhat disappointing that the solution solved by this project is focused on constructor initialization and disregarding field setters when it comes to default values. Both options are available in Dart but it seems like the Flutter paradigm of constructor arguments is beginning to take hold outside of Flutter.

I will no longer pursue the option to ignore null or missing keys during deserialization.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

emiliodallatorre picture emiliodallatorre  Â·  3Comments

e200 picture e200  Â·  5Comments

xiang23 picture xiang23  Â·  5Comments

FaisalAbid picture FaisalAbid  Â·  5Comments

ollydixon picture ollydixon  Â·  6Comments