google/built_value.dart

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 :)