Custom Mappers topic

You can create custom mappers to support custom types that are not part of the generated code. You can create a custom mapper in two ways:

  • Inherit from the low-level MapperBase and MapperElementBase interfaces, or
  • inherit from the high-level SimpleMapper interface.

For most use-cases it is recommended to use the SimpleMapper interface and only use the base interfaces when required for more advanced uses.

Simple Mapper

Create a class extending SimpleMapper<T> with T being the type that you want to decode to / encode from. Then implement the decode() and encode() methods.

A custom mapper for the Uri type would look like this:

class UriMapper extends SimpleMapper<Uri> {
  const UriMapper();
  
  @override
  Uri decode(dynamic value) {
    return Uri.parse(value as String);
  }

  @override
  dynamic encode(Uri self) {
    return self.toString();
  }
}

In the encode() method you should return a primitive serializable type, like String, int, bool, etc. or a List or Map of those types. There are also additional methods you can override, like stringify, or equals. This will enable == checks and toString on classes using this type.

Usage

To use a custom mapper, add it to the MappableClass.includeCustomMappers annotation property of the target class:

@MappableClass(includeCustomMappers: [UriMapper()])
class Domain {
  final Uri uri;

  const Domain(this.uri);
}

This will globally register the custom mapper when the class is initialized and is afterwards available for all models.

Alternatively you can manually add this to the MapperContainer.globals:

void main() {
  // Add this as the global [Uri] mapper for all models.
  MapperContainer.globals.use(UriMapper());
}

Tip: Be aware that you can also unuse() (and replace) any mappers, both custom, generated, and for primitive types. This might come in handy if you want to switch between different custom mappers for the same type.

Generic Custom Types

When dealing with generic types with one or two type parameters, you can use the SimpleMapper1 or SimpleMapper2 variants. Here the decoding and encoding functions are generic functions for receiving the correct type parameters:

You also need to construct a typeFactory as shown below.

class GenericBox<T> {
  T content;
  
  GenericBox(this.content);
}

class CustomBoxMapper extends SimpleMapper1<GenericBox> {

  @override
  // use the type parameter [T] in the return type [GenericBox<T>]
  GenericBox<T> decode<T>(dynamic value) { 
    // use the type parameter [T] in your decoding logic
    T content = container.fromValue<T>(value);
    return GenericBox<T>(container.fromValue<T>(value)); 
  }

  @override
  // use the type parameter [T] in the parameter type [GenericBox<T>]
  dynamic encode<T>(GenericBox<T> self) {
    // use the type parameter [T] in your encoding logic
    return container.toValue<T>(self.content); 
  }

  // In case of generic types, we also must specify a type factory. This is a special type of 
  // function used internally to construct generic instances of your type.
  // Specify any type arguments in alignment to the decode/encode functions.
  @override
  Function get typeFactory => <T>(f) => f<GenericBox<T>>();
}

If your generic class has one or two bounded type parameters (e.g. class NumberBox<T extends num>) use the SimpleMapper1Bounded and SimpleMapper2Bounded variants:

class NumberBox<T extends num> {
  NumberBox(this.content);

  final T content;
}

class NumberBoxMapper extends SimpleMapper1Bounded<NumberBox, num> {
  const NumberBoxMapper();

  @override
  NumberBox<A> decode<A extends num>(Object value) {
    return NumberBox<A>(container.fromValue<A>(value));
  }

  @override
  Object? encode<A extends num>(NumberBox<A> self) {
    return container.toValue(self.content);
  }

  @override
  Function get typeFactory => <T extends num>(f) => f<NumberBox<T>>();
}

Custom Iterables and Maps

For special Iterable and Map types, you can of course specify custom mappers as described in the previous section. However, we provide ready-to-use IterableMapper and MapMapper to make your life a little bit easier:

For both you have to provide

  1. a factory function, which converts a generic iterable / map to your special implementation,
  2. a type factory, similar to the one used in generic custom mappers.
void main() {
  MapperContainer.globals.use(IterableMapper<HashSet>(
    <T>(Iterable<T> i) => HashSet.of(i),
    <T>(f) => f<HashSet<T>>(),
  ));

  MapperContainer.globals.use(MapMapper<HashMap>(
    <K, V>(Map<K, V> m) => HashMap.of(m),
    <K, V>(f) => f<HashMap<K, V>>(),
  ));

  HashSet<String> brands = MapperContainer.globals.fromJson('["Toyota", "Audi", "Audi"]');
  print(String); // {"Toyota", "Audi"}
}

Advanced Uses

For more advanced uses, extend from the MapperBase class instead of the SimpleMapper.

Here the setup requires a bit more boilerplate, but you get full control over your types:

// extend [MapperBase] and provide your type
class MyClassMapper extends MapperBase<MyClass> {
  
  // If your type is generic, specify a type factory. Else this can be skipped.
  @override
  Function get typeFactory => <T>(f) => f<MyClass<T>>();
  
  // all the following overrides are optional
  
  @override
  MyClass decoder(DecodingContext<Object> context) {
    // your decoding logic here
    // you can access the encoded value using 'context.value'
    throw UnimplementedError();
  }
  
  @override
  Object? encoder(EncodingContext<Object> context) {
    // your encoding logic here
    // you can access the decoded value using 'context.value'
    throw UnimplementedError();
  }
  
  @override
  bool equals(MappingContext<Object> context, MyClass other) {
    // your equals logic here
    throw UnimplementedError();
  }
  
  @override
  int hash(MappingContext<Object> context) {
    // your hashcode logic here
    throw UnimplementedError();
  }

  @override
  String stringify(MappingContext<Object> context) {
    // your stringify logic here
    throw UnimplementedError();
  }
}

Next: Mapper Container

Classes

EnumMapper<T extends Enum> Custom Mappers
The mapper interface used for all concrete enum mappers.
IterableMapper<I extends Iterable> Custom Mappers
The default mapper for iterables like List.
MapMapper<M extends Map> Custom Mappers
The default mapper for Maps.
MappableClass Introduction Models Configuration Polymorphism Mapping Hooks Custom Mappers
Used to annotate a class in order to generate mapping code.
MapperBase<T extends Object> Custom Mappers
The common super class for all mappers.
SetMapper<S extends Set> Custom Mappers
The default mapper for Sets.
SimpleMapper<T extends Object> Custom Mappers
An interface to define a custom mapper.
SimpleMapper1<T extends Object> Custom Mappers
An interface to define custom mappers for generic types with one argument.
SimpleMapper1Bounded<T extends Object, B1> Custom Mappers
An interface to define custom mappers for generic types with one bounded argument.
SimpleMapper2<T extends Object> Custom Mappers
An interface to define custom mappers for generic types with two arguments.
SimpleMapper2Bounded<T extends Object, B1, B2> Custom Mappers
An interface to define custom mappers for generic types with two bounded arguments.

Enums

IterableEqualityMode Custom Mappers
Options for comparing an Iterable.