schultek/dart_mappable

"__type" is sometimes not included in serialized map when using MapperContainer.globals.toMap()

Opened this issue · 2 comments

Under what conditions might MapperContainer.globals.toMap() not include a __type discriminator key in a serialized object map? I have a few applications making use of this feature and sometimes __type is included and sometimes it isn't.

In this particular case, I have this model:

@MappableClass()
class Asset<T extends Object?> extends StoredObject with AssetMappable {
  Asset({
    required super.id,
    required this.userId,
    required this.value,
    required this.data,
    required this.service,
    this.metadata = const {},
    required this.createdAt,
    required this.updatedAt,
  });

  final String userId;
  final int value;
  final T? data;
  final String service;
  final Map<String, Object?> metadata;
  final DateTime createdAt;
  final DateTime updatedAt;
}

An instance of which is being serialized with this code:

  /// Upsert the object into the db
  static Future<void> saveObject<T extends StoredObject>(
    T object,
  ) async {
    final tableName = _tableMap[T];
    assert(tableName?.isNotEmpty ?? false);

    final json = MapperContainer.globals.toMap<T>(object);

    await _sb.from(tableName!).upsert(json).catchError((e) {
      throw const UserException('Error saving object');
    });
  }

When this gets serialized, the data field contains a __type key which referenced the runtime type of the generic data field at the time of serialization but the top-level map does not contain a __type key referencing the fact that the top-level object was an Asset. I am having a hard time finding out why this is when my other applications seem to correctly add the __type key automatically.

I tried to make an example that demonstrated this but ended up with opposite results. Here is what I tried:

main.dart

@MappableLib(generateInitializerForScope: InitializerScope.package)
library dmtest;

import 'package:dart_mappable/dart_mappable.dart';

import 'base.dart';
import 'main.init.dart';

void main() {
  initializeMappers();

  final a = Base<String>('a');
  final b = Base<int>(0);
  final c = Base<A>(A());

  final aMap = MapperContainer.globals.toMap<Base>(a);
  final bMap = MapperContainer.globals.toMap<Base>(b);
  final cMap = MapperContainer.globals.toMap<Base>(c);

  print(aMap);
  print(bMap);
  print(cMap);

  final a2 = MapperContainer.globals.fromMap<Base>(aMap);
  final b2 = MapperContainer.globals.fromMap<Base>(bMap);
  final c2 = MapperContainer.globals.fromMap<Base>(cMap);

  print(a2.runtimeType);
  print(a2.data.runtimeType);
  print(b2.runtimeType);
  print(b2.data.runtimeType);
  print(c2.runtimeType);
  print(c2.data.runtimeType);
}

base.dart

import 'package:dart_mappable/dart_mappable.dart';

part 'base.mapper.dart';

@MappableClass()
class Base<T extends Object?> with BaseMappable {
  final T data;

  Base(this.data);
}

@MappableClass()
class A with AMappable {
  final String test;

  A({this.test = 'test'});
}

When I run main.dart, it outputs this:

~> dart bin/main.dart
{data: a, __type: Base<String>}
{data: 0, __type: Base<int>}
{data: {test: test}, __type: Base<A>}
Base<String>
String
Base<int>
int
Base<A>
A

What is interesting is that in this example, the __type key is appearing at the top-level but not in the generic field. This is actually the behavior that I want but it is opposite from what I'm observing in the real application.

Both codebases are using these versions:
Dart: Dart SDK version: 3.2.6 (stable) (Wed Jan 24 13:41:58 2024 +0000) on "macos_arm64"
Flutter:

Flutter 3.16.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 41456452f2 (4 days ago) • 2024-01-25 10:06:23 -0800
Engine • revision f40e976bed
Tools • Dart 3.2.6 • DevTools 2.28.5

dart_mappable: 4.2.0
dart_mappable_builder: 4.2.0

After more testing, it looks like using MapperContainer.globals.toMap(object) does not include the __type key in the output but using MapperContainer.globals.toMap<dynamic>(d) does.

This behaviour is documented in the docs: https://pub.dev/documentation/dart_mappable/latest/topics/Generics-topic.html
in the Typeless Serialization section.

The important bit:

It only takes effect when the static type T and the runtime type value.runtimeType are different, in which case it will add the __type property.

This refers to the MapperContainer.globals.toMap<T>(value) method.

Thats why passing dynamic as T will always add the type property.

The same logic recurses down to fields of a class. The specific behaviour there depends on the static type of the field. If e.g. its typed as Object or dynamic it will add the type property for the field. If you use a generic parameter T for the fields it again depends on what that parameter is. From your above example encoding Asset (which is implicitly Asset<dynamic>) will probably add the type while encoding Asset<MyDataType> won't.