"__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.