Generic types & serializers
jimmyff opened this issue · 8 comments
I'm getting the following error: The argument type 'Serializer<Consumable<Object?>>' can't be assigned to the parameter type 'Serializer<Consumable<T>>'.
with this class:
abstract class Consumable<T>
implements Built<Consumable<T>, ConsumableBuilder<T>> {
// static Serializer<Consumable<T>> get serializer => _$consumableSerializer;
static Serializer<Consumable<Object?>> get serializer =>
_$consumableSerializer;
T get type;
int? get quantity;
factory Consumable([updates(ConsumableBuilder<T> b)?]) = _$Consumable<T>;
Consumable._();
factory Consumable.fromJsonMap(
Serializers serializers, Map<String, dynamic> data) {
return serializers.deserializeWith(Consumable.serializer, data)!;
}
Map<String, dynamic> toJsonMap(Serializers serializers) {
return new Map.of(serializers.serialize(this,
specifiedType: const FullType(Consumable<T>))
as Map<String, dynamic?>)
.cast<String, dynamic>();
}
}
The problem is that the serializer getter is returning type Serializer<Consumable<Object?>>
(This typing was recommended by the built_value debug errors) is causing issues with the fromJsonMap
method. I feel like I've got in to a bit of a pickle since introducing a class generic. Could you point me in the right direction?
Thanks
Hmm, where is that error thrown? If there is a stack trace, please post it :)
It's actually an analyser error so there is no stack trace. Full error object:
[{
"resource": "/Users/jimmyff/dev/rocket_kit/rocket_kit/lib/src/models/purchase_receipt.dart",
"owner": "_generated_diagnostic_collection_name_#7",
"code": {
"value": "argument_type_not_assignable",
"target": {
"$mid": 1,
"path": "/diagnostics/argument_type_not_assignable",
"scheme": "https",
"authority": "dart.dev"
}
},
"severity": 8,
"message": "The argument type 'Serializer<Consumable<Object?>>' can't be assigned to the parameter type 'Serializer<Consumable<T>>'.",
"source": "dart",
"startLineNumber": 52,
"startColumn": 40,
"endLineNumber": 52,
"endColumn": 61
}]
I've not be able to test it yet, but adding return serializers.deserializeWith( Consumable.serializer as Serializer<Consumable<T>>, data)!
seems to have satisfied the analyzer:
abstract class Consumable<T>
implements Built<Consumable<T>, ConsumableBuilder<T>> {
static Serializer<Consumable<Object?>> get serializer =>
_$consumableSerializer;
T get type;
int? get quantity;
factory Consumable([updates(ConsumableBuilder<T> b)?]) = _$Consumable<T>;
Consumable._();
factory Consumable.fromJsonMap(
Serializers serializers, Map<String, dynamic> data) {
return serializers.deserializeWith(
Consumable.serializer as Serializer<Consumable<T>>, data)!;
}
Map<String, dynamic> toJsonMap(Serializers serializers) {
return new Map.of(serializers.serialize(this,
specifiedType: const FullType(Consumable<T>))
as Map<String, dynamic?>)
.cast<String, dynamic>();
}
}
I'm midway through a bit of a refactor so I'll close the issues once I've managed to test that it doesn't throw a runtime error!
Ah, I see, I think the issue comes from a type mismatch with this line
specifiedType: const FullType(Consumable<T>)
I suggest trying
specifiedType: const FullType(Consumable<Object?>)
Thanks @davidmorgan, that might have done it, I think the generic part is working however I can't test it properly as I've run in to an enum serialization issue. For some reason it's not serializing to a String, rather an object. ExampleConsumableType.one
becomes {'$': 'ExampleConsumableType', '': 'one'}
, however if I specify the fullType when serializing then it correctly serializes it to a String. This is then affecting my Consumables serialization too. I'm sure i've just incorrectly configured my serializers?
Here's what i've got:
class ExampleConsumableType extends EnumClass {
static Serializer<ExampleConsumableType> get serializer =>
_$exampleConsumableTypeSerializer;
static const ExampleConsumableType one = _$one;
static const ExampleConsumableType two = _$two;
static const ExampleConsumableType three = _$three;
const ExampleConsumableType._(String name) : super(name);
static BuiltSet<ExampleConsumableType> get values => _$exampleValues;
static ExampleConsumableType valueOf(String name) => _$exampleValueOf(name);
static BuiltMap<ExampleConsumableType, bool> mapValue(
ExampleConsumableType value) =>
BuiltMap<ExampleConsumableType, bool>.from(
{for (var g in ExampleConsumableType.values) g: g == value});
}
abstract class Consumable<T>
implements Built<Consumable<T>, ConsumableBuilder<T>> {
// static Serializer<Consumable<T>> get serializer => _$consumableSerializer;
static Serializer<Consumable<Object?>> get serializer =>
_$consumableSerializer;
T? get type;
int? get quantity;
factory Consumable([updates(ConsumableBuilder<T> b)?]) = _$Consumable<T>;
Consumable._();
factory Consumable.fromJsonMap(
Serializers serializers, Map<String, dynamic> data) {
return serializers.deserializeWith(
Consumable.serializer as Serializer<Consumable<T>>, data)!;
}
Map<String, dynamic> toJsonMap(Serializers serializers) {
return Map.of(serializers.serialize(this,
specifiedType: const FullType(Consumable<Object?>))
as Map<String, dynamic?>)
.cast<String, dynamic>();
}
}
My tests:
void main() {
group('ExampleConsumableType', () {
final type = ExampleConsumableType.one;
test('Basic serialization', () {
final serialized = serializers.serialize(type);
expect(serialized, 'one');
});
test('Specific serialization', () {
final serialized = serializers.serialize(type,
specifiedType: FullType(ExampleConsumableType));
expect(serialized, 'one');
});
test('Deserialization', () {
final serialized = serializers.serialize(type);
final deserialized = serializers.deserialize(serialized);
expect(deserialized, ExampleConsumableType.one);
});
});
group('Consumables', () {
final consumable = Consumable<ExampleConsumableType>((b) => b
..type = ExampleConsumableType.one
..quantity = 1);
test('Basic test', () {
expect(consumable.type, ExampleConsumableType.one);
expect(consumable.quantity, 1);
});
test('Serialization', () {
final serialized = consumable.toJsonMap(serializers);
expect(serialized['type'], 'one');
expect(serialized['quantity'], 1);
});
test('Deserialization', () {
final serialized = consumable.toJsonMap(serializers);
final deserialized =
Consumable<Object?>.fromJsonMap(serializers, serialized);
expect(deserialized.type, ExampleConsumableType.one);
expect(deserialized.quantity, 1);
});
});
}
Basic serialization test on ExampleConsumableType is failing:
jimmyff@jimmys-mbp-5 rocket_kit % dart run test/models_billing.dart
00:00 +0: ExampleConsumableType Basic serialization
00:00 +0 -1: ExampleConsumableType Basic serialization [E]
Expected: 'one'
Actual: {'$': 'ExampleConsumableType', '': 'one'}
Which: not an <Instance of 'String'>
package:test_api/src/expect/expect.dart 134:31 fail
package:test_api/src/expect/expect.dart 129:3 _expect
package:test_api/src/expect/expect.dart 46:3 expect
test/models_billing.dart 14:7 main.<fn>.<fn>
package:test_api/src/backend/declarer.dart 215:19 Declarer.test.<fn>.<fn>
===== asynchronous gap ===========================
package:test_api/src/backend/declarer.dart 213:7 Declarer.test.<fn>
===== asynchronous gap ===========================
package:test_api/src/backend/invoker.dart 258:9 Invoker._waitForOutstandingCallbacks.<fn>
00:00 +0 -1: ExampleConsumableType Specific serialization
00:00 +1 -1: ExampleConsumableType Deserialization
00:00 +2 -1: Consumables Basic test
00:00 +3 -1: Consumables Serialization
00:00 +3 -2: Consumables Serialization [E]
Expected: 'one'
Actual: {'$': 'ExampleConsumableType', '': 'one'}
Which: not an <Instance of 'String'>
package:test_api/src/expect/expect.dart 134:31 fail
package:test_api/src/expect/expect.dart 129:3 _expect
package:test_api/src/expect/expect.dart 46:3 expect
test/models_billing.dart 40:7 main.<fn>.<fn>
package:test_api/src/backend/declarer.dart 215:19 Declarer.test.<fn>.<fn>
===== asynchronous gap ===========================
package:test_api/src/backend/declarer.dart 213:7 Declarer.test.<fn>
===== asynchronous gap ===========================
package:test_api/src/backend/invoker.dart 258:9 Invoker._waitForOutstandingCallbacks.<fn>
00:00 +3 -2: Consumables Deserialization
00:00 +4 -2: Some tests failed.
My serializers:
@SerializersFor([
// Generic types
BuiltList,
BuiltMap,
// Models
PurchaseTypeEnum,
Consumable,
// Enums
ExampleConsumableType,
])
final Serializers serializers = (_$serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
// for testing
..addBuilderFactory(
const FullType(Consumable, [FullType(ExampleConsumableType)]),
() => Consumable<ExampleConsumableType>())
)
.build();
If the type is not known then the serialization has to include the type in the JSON.
The serialization will work as you want if you pass the type
specifiedType: FullType(Consumable<ExampleConsumableType?>)
but you will need to somehow arrange to figure out the type in the serialization method to do this.
Ah yeah, thats what I expected. I think i'm overcomplicating everything with trying to use a generic with built values. I might rethink my approach!
Thanks for your help @davidmorgan
Agreed, if you're going to need to map statically to type for serialization, the generic doesn't add much. You could always implement
an interface with a generic if it's useful, then it doesn't get tangled with serialization concerns.
Glad I could help :)