schultek / dart_mappable

Improved json serialization and data classes with full support for generics, inheritance, customization and more.
https://pub.dev/packages/dart_mappable
MIT License
161 stars 23 forks source link

Generic custom class loses type parameter subtype #147

Closed justinbot closed 8 months ago

justinbot commented 11 months ago

Hi, I may just be missing something here -- but it seems when using a generic custom mapper for a generic container class, the container loses the subtype of its generic parameter.

Consider this example where we have a mappable class with some subtypes:

import 'package:dart_mappable/dart_mappable.dart';

part 'main.mapper.dart';

@MappableClass(discriminatorKey: 'type')
abstract class Animal with AnimalMappable {
  String name;

  Animal(this.name);
}

@MappableClass()
class Cat extends Animal with CatMappable {
  String color;

  Cat(String name, this.color) : super(name);
}

@MappableClass()
class Dog extends Animal with DogMappable {
  int age;

  Dog(String name, this.age) : super(name);
}

And a generic container class with custom mapper:

class GenericBox<T> {
  T content;

  GenericBox(this.content);
}

/// From https://pub.dev/documentation/dart_mappable/latest/topics/Custom%20Mappers-topic.html#generic-custom-types
class GenericBoxMapper extends SimpleMapper1<GenericBox> {
  const GenericBoxMapper();

  @override
  GenericBox<T> decode<T>(dynamic value) {
    T content = MapperContainer.globals.fromValue<T>(value);
    return GenericBox<T>(content);
  }

  @override
  dynamic encode<T>(GenericBox<T> self) {
    return MapperContainer.globals.toValue<T>(self.content);
  }

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

And a mappable class which includes generic containers:

@MappableClass(includeCustomMappers: [GenericBoxMapper()])
class Storage with StorageMappable {
  final List<GenericBox<Animal>> boxes;

  const Storage(this.boxes);
}

Now when a Storage instance is encoded and decoded, the GenericBox.content values have their subtypes as expected (Cat, Dog). However the issue is that each GenericBox itself is only a GenericBox<Animal>, rather than GenericBox<Cat> and GenericBox<Dog>.

void main() {
  final boxes = [
    GenericBox<Cat>(Cat('Judy', 'black')),
    GenericBox<Dog>(Dog('Greg', 5)),
  ];
  final storage = Storage(boxes);

  final storageJson = storage.toJson();

  final storageFromJson = StorageMapper.fromJson(storageJson);
  for (var box in storageFromJson.boxes) {
      print(box.runtimeType);
      print(box.content.runtimeType);
  }
  // Output:
  // GenericBox<Animal>
  // Cat
  // GenericBox<Animal>
  // Dog
}

Is it possible for the generic containers to be decoded in a way that includes their parameter subtypes?

schultek commented 11 months ago

I will look into it

schultek commented 8 months ago

@justinbot Ok so:

Reason is that you (a) use a custom mapper and (b) that encodes its content in kind of a "transparent" way, meaning in the json there is no trace of the GenericBox class. So when the decode method of the custom mapper is called T will always be the static type of the element (here Animal) and never the dynamic type, because that gets only decoded later when the content itself is decoded (by using the 'type' parameter in the animal jsons).

To also get the "real" type statically for GenericBox you can use my type_plus package that is also used internally by dart_mappable like this:

  @override
  GenericBox decode<T>(dynamic value) {
    T content = MapperContainer.globals.fromValue<T>(value);
    // This uses the `provideTo` extension from `package:type_plus` to provide the runtimeType as a static type parameter.
    return content.runtimeType.provideTo(<RuntimeT>() {
      return GenericBox<RuntimeT>(content as RuntimeT);
    });
  }