Polymorphism topic
A common pattern that you might want to use for your classes is polymorphism. As a simple example see the classes below.
abstract class Animal {
String name;
Animal(this.name);
}
class Cat extends Animal {
String color;
Cat(String name, this.color) : super(name);
}
class Dog extends Animal {
int age;
Dog(String name, this.age) : super(name);
}
Here the abstract Animal
class can either be a Cat
or a Dog
which can inherit some properties but
also introduce new ones. Now consider the following class:
class Home {
Animal pet;
Home(this.pet);
}
Now when we want to encode a Home
object, the pet
property can either be a Cat
or a Dog
. Here
two problems arise:
- When deserializing, we need to make sure that the correct subclass is chosen and instantiated, and
- when serializing, we need to make sure that the subclass information isn't lost.
To solve these, we need to add a discriminator property, that keeps track of the specific (sub)-type of the pet
.
By default no discriminator is applied, but you can change this by setting the discriminatorKey
annotation property (or globally in the build configuration).
The value of this property will default to the name of the class, but you can change this as well with the discriminatorValue
annotation property.
To reiterate: Make sure to specify the discriminatorKey
property globally or on the base class - Animal
in our example - and the discriminatorValue
property
on each of the child classes - Cat
and Dog
in our case.
// Set the discriminator key to "type".
@MappableClass(discriminatorKey: 'type')
abstract class Animal with AnimalMappable {
...
}
// Set the discriminator value for Cat to "kitty".
@MappableClass(discriminatorValue: 'kitty')
class Cat extends Animal with CatMappable {
...
}
// Here the other sub-class Dog will have the default discriminator value "Dog".
void main() {
// encode a polymorphic class
String catJson = Cat('Judy', 'black').toJson();
print(catJson); // {"name":"Judy","color":"black","type":"kitty"}
Animal myPet = AnimalMapper.fromJson(catJson); // implicit serialization as an 'Animal'
print(myPet.runtimeType); // has correct runtime type 'Cat'
// explicit serialization also works as usual without a discriminator
Cat myCat = CatMapper.fromJson('{"name": "Mindy", "color": "Brown"}');
print(myCat.name); // Mindy
}
Include Subclasses
The only slight downside of this system is that all sub-classes must be known statically at compile-time.
dart_mappable
handles this in two ways:
- All sub-classes defined in the same library as the parent class will be automatically included.
- All other sub-classes must be explicitly specified using the
includeSubClasses
property of the@MappableClass()
annotation:
// Suppose [Hamster] is defined in some other library than [Animal]
@MappableClass(discriminatorKey: 'type', includeSubClasses: [Hamster])
abstract class Animal with AnimalMappable {
...
}
Null and Default Values
There are two additional cases that you might want to cover. Either your discriminator property is null
, or some other value that you did not specify.
- For the
null
case, you can explicitly set thediscriminatorValue
property tonull
and this will work as expected. - For the other case, you can specify a fallback class to be used whenever we come across an unknown discriminator value. On this class set the
discriminatorValue
toMappableClass.useAsDefault
.
// This sub-class will be chosen when the discriminator is null.
@MappableClass(discriminatorValue: null)
class NullAnimal extends Animal with NullAnimalMappable {
NullAnimal(String name) : super(name);
}
// This sub-class will be chosen on any unknown discriminator.
@MappableClass(discriminatorValue: MappableClass.useAsDefault)
class DefaultAnimal extends Animal with DefaultAnimalMappable {
String type;
DefaultAnimal(String name, this.type) : super(name);
}
void main() {
// decode json with the discriminator set to null (same as missing property)
Animal animal1 = AnimalMapper.fromJson('{"name": "Scar", "type": null}');
print(animal1.runtimeType); // NullAnimal
// decode json with unknown discriminator value
Animal animal2 = AnimalMapper.fromJson('{"name": "Balu", "type": "Bear"}');
print(animal2.runtimeType); // DefaultAnimal
print(animal2.type); // Bear
}
Custom Discriminator Logic
For a more advanced use-case where the discriminator key/value system is not enough, you can use the
CheckTypesHook
to define custom discriminator checks on your subclasses.
Instead of giving each subclass a discriminator value, each subclass can have a custom function that checks whether the encoded value should be decoded to this subclass and returns a boolean.
@MappableClass(
hooks: CheckTypesHook({
B: B.checkType,
C: C.checkType,
}),
)
abstract class A with AMappable {
A();
}
@MappableClass()
class B extends A with BMappable {
B();
/// checks if [value] should be decoded to [B]
static bool checkType(value) {
return value is Map && value['isB'] == true;
}
}
@MappableClass()
class C extends A with CMappable {
C();
/// checks if [value] should be decoded to [C]
static bool checkType(value) {
return value is Map && value['isWhat'] == 'C';
}
}
Classes
- CheckTypesHook Polymorphism Mapping Hooks
- A MappingHook that allows to specify custom type checks for serialization polymorph subclasses of a class.
- MappableClass Introduction Models Configuration Polymorphism Mapping Hooks Custom Mappers
- Used to annotate a class in order to generate mapping code
- MappableLib Configuration Polymorphism Generics
- Used to annotate a library to define default values.