Creating Custom built_value Serializers in Dart
When using built_value for JSON serializing and deserializing, we might have some cases that go beyond the capabilities of StandardJsonPlugin
.
Imagine the following problem: you have an API endpoint that can give you two different types of data structures for the same value as shown in the following example:
{
"value": "String value"
}
{
"value": ["String 1", "String 2"]
}
In our flutter app, we might want to have an object that can have both values, and then we decide what we show in our Widgets.
class CustomValue {
String singleValue;
List<String> multipleValues;
}
So, how can we map the String value to singleValue and the array of Strings to multipleValues? With a CustomSerialize. If we inspect the Serializer class, it’s stated:
must extend either [PrimitiveSerializer] or/
[StructuredSerializer]./
Since our data structure is not a primitive object, we’ll have to create a class that implements StructuredSerializer
.
class CustomValueSerializer
implements StructuredSerializer<CustomValue> {
@override
CustomValue deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) {
// TODO: implement deserialize
return null;
}
@override
Iterable serialize(Serializers serializers, CustomValue object, {FullType specifiedType = FullType.unspecified}) {
// TODO: implement serialize
return null;
}
@override
// TODO: implement types
Iterable<Type> get types => null;
@override
// TODO: implement wireName
String get wireName => null;
}
Let’s inspect each method we need to implement:
types are the types of objects that can be serialized. When using built_value, it will generate an internal type called _$CustomValue
that must also be serialized, so we have:
@override
Iterable<Type> get types => [CustomValue, _$CustomValue];
wirename
is the name of the class we want to serialize
@override
String get wireName => "CustomValue";
Finally, we must implement the serialize
and deserialize
methods. Here we will be able to check if the value that we receive is of type String
or type List
and map it to the correct value of CustomValue
. To do this, we need to check how the generated code of a similar class is structured, and adjust it to fit our needs. In this case, when we are checking the value
field, we are not assigning it directly, but checking first which type to attribute to the variable, to either assign it to String value
or List<String>
values.
However, since we are using built_value
, the type of list we will be dealing with comes from the built_collection package, hence we will be declaring it as BuiltList<String>
values.
@override
CustomValue deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
// Initialize an empty builder
final result = new CustomValueBuilder();
// Create an `Iterator` from the serialized data received
final iterator = serialized.iterator;
// Loop the iterator for each key
while (iterator.moveNext()) {
final key = iterator.current as String;
iterator.moveNext();
final dynamic value = iterator.current;
// for each key, assign the correct value to the builder
switch (key) {
case 'value':
// If the value is of type List<dynamic>, assign it to `values`
if (value is List<dynamic>) {
result.values.replace(serializers.deserialize(value,
specifiedType: const FullType(BuiltList, const [
const FullType(String)
])) as BuiltList);
// else, the value is of type `String`
} else {
result.value = serializers.deserialize(value.toString(),
specifiedType: const FullType(String)) as String;
}
break;
}
}
return result.build();
}
@override
Iterable serialize(Serializers serializers, CustomValue object,
{FullType specifiedType = FullType.unspecified}) {
// Create an empty object array
final result = <Object>[];
// if the value of the `CustomValue` is not null, then assign it to a String
if (object.value != null) {
result
..add('value')
..add(serializers.serialize(object.value,
specifiedType: const FullType(String)));
}
// Else, it means that we have a list. In this case the list will always override
// the defined String value
if (object.values != null) {
result
..add('values')
..add(serializers.serialize(object.values,
specifiedType:
const FullType(BuiltList, const [const FullType(String)])));
}
return result;
}
Now, since we have our CustomValueSerializer
class, we can start working on the CustomValue
class.
part 'custom_value.g.dart';
abstract class CustomValue implements Built<CustomValue, CustomValueBuilder> {
static Serializer<CustomValue> get serializer => null; // todo
@nullable String get value;
@nullable BuiltList<String> get values;
CustomValue._();
factory CustomValue([updates(CustomValueBuilder b)]) = _$CustomValue;
}
The setup of the class is equal to a class that uses the StandardJsonPlugin
, the only difference being in the way we declare the serializer
. In this case, we can use the new annotation @BuiltValueSerializer
to say to the serializer “hey, we are using a custom serializer, don’t generate one for this class”.
@BuiltValueSerializer(custom: true)
static Serializer<CustomValue> get serializer => CustomDataSerializer();
What’s missing?
Our Serializers
class that declares all the classes to be serialized in the project. For the case of custom serializers, we don’t need to put additional information in this class, so we can initialize it as we usually do.
part 'serializers.g.dart';
@SerializersFor(const [
CustomValue
])
Serializers serializers = _$serializers;
Serializers standardSerializers =
(serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
).build();
Finally, we can run build_runner
in the terminal to generate all the new files:
flutter packages pub run build_runner watch
Et voilá! We have successfully used a custom serializer with built_value
! 🎉
As a bonus, we can guarantee that everything is working by writing some good-old-fashioned unit tests:
test("Single value", () {
var value = "test";
var jsonMap = '{"value": "$value"}';
var encodedJson = json.jsonDecode(jsonMap);
CustomValue customValue = standardSerializers.deserializeWith(CustomValue.serializer, encodedJson);
expect(customValue.value, equals(value));
expect(customValue.values, isNull);
});
test("Multiple values", () {
var value1 = "test";
var value2 = "system";
var value = '["$value1", "$value2"]';
var jsonMap = '{"value": $value}';
var encodedJson = json.jsonDecode(jsonMap);
CustomValue customValue = standardSerializers.deserializeWith(CustomValue.serializer, encodedJson);
expect(customValue.value, isNull);
expect(customValue.values, equals([value1, value2]));
});
All tests pass, and we are good to go.
You can see the full example in the GitHub Repo.
Want to get the latest articles and news? Subscribe to the newsletter here 👇
And for other articles, check the rest of the blog! Blog - Gonçalo Palma