Skip to content

Add mutable collection serializers: List, Map, Set. #1360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

# 8.10.0

- Add mutable collection serializers: `List`, `Set`, `Map`. These let you
easily serialize mutable collections of immutable value types; it's still
recommended to avoid mutable collections inside value types, as they break
hashing, comparison and caching.
- Stop generating unnecessary `new` keywords.
- Stop generating explicit null checks in constructors: these are not needed
with sound null safety.
Expand Down
6 changes: 6 additions & 0 deletions built_value/lib/serializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import 'package:built_value/src/duration_serializer.dart';
import 'package:built_value/src/int32_serializer.dart';
import 'package:built_value/src/int64_serializer.dart';
import 'package:built_value/src/json_object_serializer.dart';
import 'package:built_value/src/list_serializer.dart';
import 'package:built_value/src/map_serializer.dart';
import 'package:built_value/src/num_serializer.dart';
import 'package:built_value/src/set_serializer.dart';
import 'package:built_value/src/uint8_list_serializer.dart';
import 'package:built_value/src/uri_serializer.dart';

Expand Down Expand Up @@ -59,6 +62,9 @@ abstract class Serializers {
return (SerializersBuilder()
..add(BigIntSerializer())
..add(BoolSerializer())
..add(ListSerializer())
..add(MapSerializer())
..add(SetSerializer())
..add(BuiltListSerializer())
..add(BuiltListMultimapSerializer())
..add(BuiltMapSerializer())
Expand Down
49 changes: 49 additions & 0 deletions built_value/lib/src/list_serializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2025, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';

class ListSerializer implements StructuredSerializer<List> {
final bool structured = true;
@override
final Iterable<Type> types = BuiltList<Type>([List, <Object>[].runtimeType]);
@override
final String wireName = 'List';

@override
Iterable<Object?> serialize(Serializers serializers, List list,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;
if (!isUnderspecified) serializers.expectBuilder(specifiedType);

var elementType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];

return list
.map((item) => serializers.serialize(item, specifiedType: elementType));
}

@override
List deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;

var elementType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];

var result = isUnderspecified
? <Object>[]
: serializers.newBuilder(specifiedType) as List;

for (final item in serialized) {
result.add(serializers.deserialize(item, specifiedType: elementType));
}
return result;
}
}
70 changes: 70 additions & 0 deletions built_value/lib/src/map_serializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) 2025, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';

class MapSerializer implements StructuredSerializer<Map> {
final bool structured = true;
@override
final Iterable<Type> types =
BuiltList<Type>([Map, <Object, Object>{}.runtimeType]);
@override
final String wireName = 'Map';

@override
Iterable<Object?> serialize(Serializers serializers, Map Map,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;
if (!isUnderspecified) serializers.expectBuilder(specifiedType);

var keyType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];
var valueType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[1];

var result = <Object?>[];
for (var key in Map.keys) {
result.add(serializers.serialize(key, specifiedType: keyType));
final value = Map[key];
result.add(serializers.serialize(value, specifiedType: valueType));
}
return result;
}

@override
Map deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;

var keyType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];
var valueType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[1];

var result = isUnderspecified
? <Object, Object>{}
: serializers.newBuilder(specifiedType) as Map;

if (serialized.length % 2 == 1) {
throw ArgumentError('odd length');
}

for (var i = 0; i != serialized.length; i += 2) {
final key = serializers.deserialize(serialized.elementAt(i),
specifiedType: keyType);
final value = serializers.deserialize(serialized.elementAt(i + 1),
specifiedType: valueType);
result[key] = value;
}

return result;
}
}
49 changes: 49 additions & 0 deletions built_value/lib/src/set_serializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2025, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';

class SetSerializer implements StructuredSerializer<Set> {
final bool structured = true;
@override
final Iterable<Type> types = BuiltSet<Type>([Set, <Object>{}.runtimeType]);
@override
final String wireName = 'Set';

@override
Iterable<Object?> serialize(Serializers serializers, Set set,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;
if (!isUnderspecified) serializers.expectBuilder(specifiedType);

var elementType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];

return set
.map((item) => serializers.serialize(item, specifiedType: elementType));
}

@override
Set deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
var isUnderspecified =
specifiedType.isUnspecified || specifiedType.parameters.isEmpty;

var elementType = specifiedType.parameters.isEmpty
? FullType.unspecified
: specifiedType.parameters[0];

var result = isUnderspecified
? <Object>{}
: serializers.newBuilder(specifiedType) as Set;

for (final item in serialized) {
result.add(serializers.deserialize(item, specifiedType: elementType));
}
return result;
}
}
109 changes: 109 additions & 0 deletions built_value/test/list_serializer_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2025, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:built_value/serializer.dart';
import 'package:test/test.dart';

void main() {
group('List with known specifiedType but missing builder', () {
var data = <int>[1, 2, 3];
var specifiedType = const FullType(List, [FullType(int)]);
var serializers = Serializers();
var serialized = json.decode(json.encode([1, 2, 3])) as Object;

test('serialize throws', () {
expect(() => serializers.serialize(data, specifiedType: specifiedType),
throwsA(const TypeMatcher<StateError>()));
});

test('deserialize throws', () {
expect(
() =>
serializers.deserialize(serialized, specifiedType: specifiedType),
throwsA(const TypeMatcher<DeserializationError>()));
});
});

group('List with known specifiedType and correct builder', () {
var data = <int>[1, 2, 3];
var specifiedType = const FullType(List, [FullType(int)]);
var serializers = (Serializers().toBuilder()
..addBuilderFactory(specifiedType, () => <int>[]))
.build();
var serialized = json.decode(json.encode([1, 2, 3])) as Object;

test('can be serialized', () {
expect(serializers.serialize(data, specifiedType: specifiedType),
serialized);
});

test('can be deserialized', () {
expect(serializers.deserialize(serialized, specifiedType: specifiedType),
data);
});

test('keeps generic type when deserialized', () {
expect(
serializers
.deserialize(serialized, specifiedType: specifiedType)
.runtimeType,
<int>[].runtimeType);
});
});

group('List nested with known specifiedType and correct builders', () {
var data = <List<int>>[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
var specifiedType = const FullType(List, [
FullType(List, [FullType(int)])
]);
var serializers = (Serializers().toBuilder()
..addBuilderFactory(specifiedType, () => <List<int>>[])
..addBuilderFactory(
const FullType(List, [FullType(int)]), () => <int>[]))
.build();
var serialized = json.decode(json.encode([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])) as Object;

test('can be serialized', () {
expect(serializers.serialize(data, specifiedType: specifiedType),
serialized);
});

test('can be deserialized', () {
expect(serializers.deserialize(serialized, specifiedType: specifiedType),
data);
});
});

group('List with unknown specifiedType and no builders', () {
var data = <int>[1, 2, 3];
var specifiedType = FullType.unspecified;
var serializers = Serializers();
var serialized = json.decode(json.encode([
'List',
['int', 1],
['int', 2],
['int', 3]
])) as Object;

test('can be serialized', () {
expect(serializers.serialize(data, specifiedType: specifiedType),
serialized);
});

test('can be deserialized', () {
expect(serializers.deserialize(serialized, specifiedType: specifiedType),
data);
});
});
}
Loading