Skip to content

Commit

Permalink
Allow types with record fields to be serialized via typedef and custo…
Browse files Browse the repository at this point in the history
…m serializer.
  • Loading branch information
davidmorgan committed Nov 24, 2023
1 parent 53ae619 commit cc8ca31
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

# 8.8.0 (unreleased)

- Allow classes with record fields to be serialized if they use a typedef for
the record type and install a custom `Serializer` for it.

# 8.7.0

- Add `Int32` serializer.
Expand Down
16 changes: 13 additions & 3 deletions built_value/lib/src/built_json_serializers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class BuiltJsonSerializers implements Serializers {
if (specifiedType.isUnspecified) {
final serializer = serializerForType(object.runtimeType);
if (serializer == null) {
throw StateError("No serializer for '${object.runtimeType}'.");
throw StateError(
_noSerializerMessageFor(object.runtimeType.toString()));
}
if (serializer is StructuredSerializer) {
final result = <Object?>[serializer.wireName];
Expand Down Expand Up @@ -135,7 +136,7 @@ class BuiltJsonSerializers implements Serializers {

final serializer = serializerForWireName(wireName);
if (serializer == null) {
throw StateError("No serializer for '$wireName'.");
throw StateError(_noSerializerMessageFor(wireName));
}

if (serializer is StructuredSerializer) {
Expand Down Expand Up @@ -164,7 +165,8 @@ class BuiltJsonSerializers implements Serializers {
// Might be an interface; try resolving using the type on the wire.
return deserialize(objectBeforePlugins);
} else {
throw StateError("No serializer for '${specifiedType.root}'.");
throw StateError(
_noSerializerMessageFor(specifiedType.root.toString()));
}
}

Expand Down Expand Up @@ -321,3 +323,11 @@ String _getRawName(Type? type) {
var genericsStart = name.indexOf('<');
return genericsStart == -1 ? name : name.substring(0, genericsStart);
}

String _noSerializerMessageFor(String typeName) {
final maybeRecordAdvice = typeName.contains('(')
? ' Note that record types are not automatically serializable, '
'please write and install your own `Serializer`.'
: '';
return "No serializer for '$typeName'.$maybeRecordAdvice";
}
12 changes: 9 additions & 3 deletions built_value_generator/lib/src/serializer_source_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,16 @@ abstract class SerializerSourceField
];
}

if (isSerializable && element.getter!.returnType is RecordType) {
if (isSerializable &&
element.getter!.returnType is RecordType &&
typeWithPrefix.contains('(')) {
return [
'Record fields are not (yet) serializable. '
'Remove "$name" or mark it "@BuiltValueField(serialize: false)".'
'Fields declared with record types are not automatically serializable. '
'To allow the class to be serialized, modify "$name" by: '
'a) removing it, or '
'b) marking it "@BuiltValueField(serialize: false)", or '
'c) creating a `typedef` to represent the record type then '
'writing and installing a custom `Serializer` for it.'
];
}

Expand Down
6 changes: 3 additions & 3 deletions built_value_generator/test/serializer_generator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ abstract class Value implements Built<Value, ValueBuilder> {
factory Value([void Function(ValueBuilder) updates]) = _$Value;
}
'''),
contains(r'1. Record fields are not (yet) serializable. '
'Remove "record" or mark it '
'"@BuiltValueField(serialize: false)".'));
contains(r'1. Fields declared with record types are not '
'automatically serializable. '
'To allow the class to be serialized, modify "record" by'));
});
});
}
Expand Down
26 changes: 24 additions & 2 deletions end_to_end_test/lib/records.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ abstract class ComplexRecordValue
ComplexRecordValue._();
}

typedef RecordOfIntInt = (int, int);

// Record serialization is not supported, so a class with records is only
// serializable if record fields are not serialized.
abstract class SerializableRecordValue
Expand All @@ -63,11 +65,31 @@ abstract class SerializableRecordValue
_$serializableRecordValueSerializer;

int get value;
@BuiltValueField(serialize: false)
(int, int)? get record;
RecordOfIntInt? get record;

factory SerializableRecordValue(
[void Function(SerializableRecordValueBuilder) updates]) =
_$SerializableRecordValue;
SerializableRecordValue._();
}

class RecordOfIntIntSerializer implements StructuredSerializer<RecordOfIntInt> {
@override
RecordOfIntInt deserialize(
Serializers serializers, Iterable<Object?> serialized,
{FullType specifiedType = FullType.unspecified}) {
return (serialized.elementAt(0)! as int, serialized.elementAt(1)! as int);
}

@override
Iterable<Object?> serialize(Serializers serializers, RecordOfIntInt object,
{FullType specifiedType = FullType.unspecified}) {
return [object.$1, object.$2];
}

@override
Iterable<Type> get types => [RecordOfIntInt];

@override
String get wireName => 'RecordOfIntInt';
}
21 changes: 16 additions & 5 deletions end_to_end_test/lib/records.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion end_to_end_test/test/records_serializer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

import 'dart:convert';

import 'package:end_to_end_test/errors_matchers.dart';
import 'package:end_to_end_test/records.dart';
import 'package:end_to_end_test/serializers.dart';
import 'package:test/test.dart';

void main() {
group(SerializableRecordValue, () {
group('$SerializableRecordValue with no record value', () {
var data = SerializableRecordValue((b) => b..value = 1);
var serialized = json.decode(json.encode([
'SerializableRecordValue',
Expand All @@ -25,4 +26,41 @@ void main() {
expect(serializers.deserialize(serialized), data);
});
});

group('$SerializableRecordValue with a record value', () {
var data = SerializableRecordValue((b) => b
..value = 1
..record = (1, 2));
var serialized = json.decode(json.encode([
'SerializableRecordValue',
'value',
1,
'record',
[1, 2],
])) as Object;
var serializersWithCustomSerializer =
(serializers.toBuilder()..add(RecordOfIntIntSerializer())).build();

test('gives advice about custom serializer on failure to serialize', () {
expect(
() => serializers.serialize(data),
throwsA(isErrorContaining(
'record types are not automatically serializable')));
});

test('gives advice about custom serializer on failure to deserialize', () {
expect(
() => serializers.deserialize(serialized),
throwsA(isErrorContaining(
'record types are not automatically serializable')));
});

test('can be serialized with custom serializer', () {
expect(serializersWithCustomSerializer.serialize(data), serialized);
});

test('can be deserialized with custom deserializer', () {
expect(serializersWithCustomSerializer.deserialize(serialized), data);
});
});
}

0 comments on commit cc8ca31

Please sign in to comment.