schultek/dart_mappable

Issue regarding Maps with null fields a generic custom mapper on third party class

Closed this issue · 2 comments

Thanks you for this great package. It has been a delight to use.

When creating a generic custom mapper for the Option type from fpdart everything works well for toMap and fromMap when the field is not null. However, when the field of the Map is null, I get the following error: MapperException: Failed to decode (ClassWithOption).optional: Parameter optional is missing.

Here is the custom mapper:

class OptionConverter extends SimpleMapper1<Option> {
  const OptionConverter();
  @override
  Option<T> decode<T>(dynamic value) {
    final content = MapperContainer.globals.fromValue<T>(value);
    return content == null ? none<T>() : some<T>(content);
  }

  @override
  dynamic encode<T>(Option<T> self) {
    return MapperContainer.globals.toValue<T>(self.match(() => null, (t) => t));
  }

  @override
  Function get typeFactory => <T>(f) => f<Option<T>>();
}

and here is a snippet of a class using the custom mapper:

@MappableClass(includeCustomMappers: [OptionConverter()])
class ClassWithOption with ClassWithOptionMappable {
  ClassWithOption({
    required this.optional,
    required this.name,
  });

  final Option<String> optional;
  final String name;

  static const fromMap = ClassWithOptionMapper.fromMap;
}

It fails with the following snippet:

ClassWithOption.fromMap({'name': 'bob', 'optional': null})

I cannot tell if I am doing something wrong or if this is a bug.

Thanks again!

The reason this fails is because the null check (that triggers the MapperException you get) is done before invoking the mapper. This is because usually a mapper only works with non-null values. Option here is a special case.

I would propose a workaround to use a default value in the constructor like this:

class ClassWithOption with ClassWithOptionMappable {
  ClassWithOption({
    Option<String>? optional,
    required this.name,
  }) : optional = optional ?? none();

  final Option<String> optional;
  final String name;

  static const fromMap = ClassWithOptionMapper.fromMap;
}

Thanks for the solution. It works as intended.