Custom Serializer with built_value in Dart and Flutter
Tags: flutter, dart, tutorial, programming
Published: 2023-02-21 10:31:13


The package, [`built_value`](https://pub.dev/packages/built_value) is used for creating immutable classes in Dart with JSON serialization. It's a great package and uses `build_runner` to create generated files to avoid boilerplate code.  
But if you've ever tried to deserialize a model class with a collection generic type like `BuiltList`, it'll drive you mad!

So, here is our goal. We're going to customize `build_value`'s serializer such that it can deserialize properties like `BuiltList<T> get data`.

## Overview

- [Application](#application)
- [Setting up Models](#setup)
- [What's the problem?](#problem)
- [Finally, a solution!](#solution)
  - [1. Adding `specifiedType`](#1-solution)
  - [2. Adding `addBuilderFactory`](#2-solution)
- [This solution works. Why?](#reason)
- [Simplifying the solution](#simplification)
- [Going one step further](#futher)
- [Conclusion](#conclusion)
- [Final Note](#final-note)

> Here's the [GitHub repo](https://github.com/nicks101/flutter_tutorials/tree/main/built_value_generic) with all the code.

### Application <a id="application"></a>

The inspiration for this is a straightforward use case:  
Creating a generic pagination model with the `BuiltList<T> get data` property.  
So all paginated APIs can use this one model with a different data type for the data field. Rest of the fields like `current_page`, `next_page`, etc. are common for all paginated APIs.

## Setting up Models <a id="setup"></a>

Create a file called `generic_model.dart`. Using the snippets `bvtgsf` from the `built_value_snippets` package in VS Code (or any other IDE), we get the following class.  
I've made some changes to it as required.

```dart
/// Add the import for the serializer
import 'serializers.dart';

part 'generic_model.g.dart';

abstract class GenericModel<T>
    implements Built<GenericModel<T>, GenericModelBuilder<T>> {
  GenericModel._();
  factory GenericModel([void Function(GenericModelBuilder<T>) updates]) =
      _$GenericModel<T>;

  Map<String, dynamic> toJson() {
    /// Add the typecast [Map<String, dynamic>] to fix the error
    return serializers.serializeWith(GenericModel.serializer, this)
        as Map<String, dynamic>;
  }

  /// Add [<T>] to deserialize generic type values
  static GenericModel<T> fromJson<T>(Map<String, dynamic> json) {
    /// Add the typecast [GenericModel<T>] to fix the error
    return serializers.deserializeWith(GenericModel.serializer, json)
        as GenericModel<T>;
  }

  static Serializer<GenericModel> get serializer => _$genericModelSerializer;

  BuiltList<T> get data;
}
```

Let's complete the serializer class setup.

```dart
part 'serializers.g.dart';

@SerializersFor([
  GenericModel,
])
final Serializers serializers =
    (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
```

And last, let's generate the code by using the following command.  
`dart run build_runner build` or  
`flutter pub run build_runner build` if you have a Flutter project.

> If you get any errors with this command, try it with the following parameters:  
> `dart run build_runner build --delete-conflicting-outputs`

And we're done with the initial setup.

## What's the problem? <a id="problem"></a>

Now we'll test the serialization and deserialization with the generic type `String`. We'll create the class with a list of strings and try to serialize it.

```dart
void main() {
  /// Testing serialization generic type [String]
  final testModel = GenericModel<String>(
    (b) => b
      ..data = ListBuilder<String>(
        ['string 1', 'string 2'],
      ),
  );

  print('model: $testModel');
  print('serialized json: ${testModel.toJson()}');
}
```

Output:

```bash
model: GenericModel {
  data=[string 1, string 2],
}
serialized json: {data: [{$: String, : string 1}, {$: String, : string 2}]}
```

So this looks good. It worked.  
But wait, we have to test deserialization as well. This won't work but don't go on my word. I'll show you.

```dart
void main() {
  /// Testing serialization with generic type [String]
  final testModel = GenericModel<String>(
    (b) => b
      ..data = ListBuilder<String>(
        ['string 1', 'string 2'],
      ),
  );

  print('model: $testModel');
  print('serialized json: ${testModel.toJson()}');

  /// Testing deserialization with generic type [String]
  final dummyJson = {
    'data': ['string 1', 'string 2'],
  };
  try {
    final testString = GenericModel.fromJson<String>(dummyJson);

    print(testString);
    print(testString.toJson());
  } catch (e) {
    print("Looks like it didn't work 🙃");
    print(e);
  }
}
```

Output:

```bash
model: GenericModel {
  data=[string 1, string 2],
}
serialized json: {data: [{$: String, : string 1}, {$: String, : string 2}]}
Looks like it didn't work 🙃
Deserializing '[data, [string 1, string 2]]' to 'GenericModel' failed due to: Deserializing '[string 1, string 2]' to 'BuiltList<Object>' failed due to: Bad state: No serializer for 'Object'.
```

The deserialization doesn't work. It's because `built_value` cannot detect the data type of the object to be created.  
Hence we have to tell it explicitly.

## Finally, a solution! <a id="solution"></a>

There are two things we need to add to fix this.  
First, the `specifiedType` and second, the `addBuilderFactory`.

### 1. Adding `specifiedType` <a id="1-solution"></a>
    

In `GenericModel` class, we will explicitly tell the serializer to deserialize the `T` type.

```dart
abstract class GenericModel<T>
    implements Built<GenericModel<T>, GenericModelBuilder<T>> {
  GenericModel._();
 ...

  static GenericModel<T> fromJson<T>(Map<String, dynamic> json) {
/// <-------- Added here
    return serializers.deserialize(
      json,
      specifiedType: FullType(GenericModel, [FullType(T)]),
    ) as GenericModel<T>;
/// <-------- Added here
  }

  static Serializer<GenericModel> get serializer => _$genericModelSerializer;

  BuiltList<T> get data;
}
```

Before moving to step 2, let's run the code.

```bash
model: GenericModel {
  data=[string 1, string 2],
}
serialized json: {data: [{$: String, : string 1}, {$: String, : string 2}]}

Looks like it didn't work 🙃
Deserializing '[data, [string 1, string 2]]' to 'GenericModel<String>' failed due to: Bad state: No builder factory for GenericModel<String>. Fix by adding one, see SerializersBuilder.addBuilderFactory.
```

Progress, we have a different error now.  
The error mentions `addBuilderFactory`, the same thing that we need for step 2.

### 1. Adding `addBuilderFactory` <a id="2-solution"></a>
    

This will be added to the `serializer.dart` class.

```dart
@SerializersFor([
  GenericModel,
])
final Serializers serializers = (_$serializers.toBuilder()
      ..addPlugin(StandardJsonPlugin())
/// <-------- Added here
      ..addBuilderFactory(
        const FullType(GenericModel, [FullType(String)]),
        () => GenericModelBuilder<String>(),
      )
      ..addBuilderFactory(
        const FullType(BuiltList, [FullType(String)]),
        () => ListBuilder<String>(),
      ))
/// <-------- Added here
    .build();
```

Now, if we run the code everything works. Yay! 🎉

```bash
model: GenericModel {
  data=[string 1, string 2],
}
serialized json: {data: [{$: String, : string 1}, {$: String, : string 2}]}
GenericModel {
  data=[string 1, string 2],
}
{data: [{$: String, : string 1}, {$: String, : string 2}]}
```

## This solution works. Why? <a id="reason"></a>

`built_value` doesn't add a `builder` and `specifiedType` for generic values.  
It is up to the developers how they want to define it.

In the `test` file of the `built_value` package, they have defined such a case [here](https://github.com/google/built_value.dart/blob/master/end_to_end_test/test/generics_nnbd_serializer_test.dart#L36).

```dart
/// code snippet from the generics_serializer_test.dart file at line 38
group('GenericValue with known specifiedType and correct builder', () {
    var data = GenericValue<int>((b) => b..value = 1);
    var specifiedType = const FullType(GenericValue, [FullType(int)]);

   /// defining a different serializer
    var serializersWithBuilder = (serializers.toBuilder()
          ..addBuilderFactory(specifiedType, () => GenericValueBuilder<int>()))
        .build();
    var serialized = json.decode(json.encode([
      'value',
      1,
    ])) as Object;

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

    test('can be deserialized', () {   /// <--- using different serializer
      expect(
          serializersWithBuilder.deserialize(serialized,
              specifiedType: specifiedType),
          data);
    });
```

The deserialization is done using a custom serializer object which contains a new builder and a specified type.

If we were to use generic value with collections (like `BuiltList`) we need another builder. This is mentioned in [this](https://github.com/google/built_value.dart/blob/master/end_to_end_test/test/generics_nnbd_serializer_test.dart#L194) test.

```dart
/// code snippet from the generics_serializer_test.dart file at line 197
group('CollectionGenericValue with known specifiedType and correct builder',
      () {
    var data = CollectionGenericValue<int>((b) => b..values.add(1));
    var specifiedType = const FullType(CollectionGenericValue, [FullType(int)]);
    var serializersWithBuilder = (serializers.toBuilder()
          ..addBuilderFactory(
              specifiedType, () => CollectionGenericValueBuilder<int>())

          /// adding second builder for collection
          ..addBuilderFactory(const FullType(BuiltList, [FullType(int)]),
              () => ListBuilder<int>()))
        .build();
...
```

In addition to a custom serializer, we add another `builderFactory` for the `BuiltList` collection.

We can of course create as many customizations now as needed by adding `specifiedType` and `builderFactory` methods.

> Refer the test file [here](https://github.com/google/built_value.dart/blob/master/end_to_end_test/test/generics_nnbd_serializer_test.dart) for more examples.

## Simplifying the solution <a id="simplification"></a>

Is this the end?  
It depends.  
We have found the solution but imagine you have to use the `GenericModel` class with not just one but multiple object types. We could copy-paste the builder factory to the `serializer.dart` class, but that's a bad idea.  
Let me show you why.

```dart
final Serializers serializers = (_$serializers.toBuilder()
      ..addPlugin(StandardJsonPlugin())

      /// for [String]
      ..addBuilderFactory(
        const FullType(GenericModel, [FullType(String)]),
        () => GenericModelBuilder<String>(),
      )
      ..addBuilderFactory(
        const FullType(BuiltList, [FullType(String)]),
        () => ListBuilder<String>(),
      )

      /// for [int]
      ..addBuilderFactory(
        const FullType(GenericModel, [FullType(int)]),
        () => GenericModelBuilder<int>(),
      )
      ..addBuilderFactory(
        const FullType(BuiltList, [FullType(int)]),
        () => ListBuilder<int>(),
      )

    /// and so on...
```

Manually adding and removing them can introduce various bugs. Plus we're lazy and don't want to remember updating this.  
Hence, we do what we do best -&gt; automate it.

Automate is just a buzz keyword here, we're just going to abstract adding `builderfactory` for `GenericModel`.

We'll add a new class to the `serializers.dart` file which adds the builder factories to `serializer` object.

```dart
@SerializersFor([
  GenericModel,
])

/// Making this private and mutable
Serializers _serializers =
    (_$_serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

final serializers = _serializers;

/// Class to add new factories according to the type
class GenericBuilderFactory<T> {
  /// Keeping track of the builder factories added to the serializers for type T
  static final Map<Type, GenericBuilderFactory> _factories = {};

  /// returning serializer if the builder factory is already added
  static Serializers getSerializer<T>() {
    if (_factories.containsKey(T)) {
      return _serializers;
    }
    return GenericBuilderFactory<T>()._addFactory();
  }

  /// Adding the builder factory
  Serializers _addFactory() {
    _factories[T] = this;

    _serializers = (_serializers.toBuilder()
          ..addBuilderFactory(
            FullType(GenericModel, [FullType(T)]),
            () => GenericModelBuilder<T>(),
          )
          ..addBuilderFactory(
            FullType(BuiltList, [FullType(T)]),
            () => ListBuilder<T>(),
          ))
        .build();

    return _serializers;
  }
}
```

Next, we call this static method to access `serializer` object in our `GenericModel` class.

```dart
...
  static GenericModel<T> fromJson<T>(Map<String, dynamic> json) {
    return GenericBuilderFactory.getSerializer<T>().deserialize(
      json,
      specifiedType: FullType(GenericModel, [FullType(T)]),
    ) as GenericModel<T>;
  }
...
```

We made a lot of changes, let's make sure our old tests are still passing.  
Run the same `builder_runner` command and run the project with the `dart run` command.

Output:

```bash
GenericModel {
  data=[string 1, string 2],
}
{data: [{$: String, : string 1}, {$: String, : string 2}]}
```

Phew! The old code is working.  
Let's try adding a test case for `int` type to make sure it's working.

```dart
...
/// Testing deserialization with generic type [String]
  final dummyJson = {
    'data': ['string 1', 'string 2'],
  };
  try {
    final testString = GenericModel.fromJson<String>(dummyJson);

    print(testString);
    print(testString.toJson());
  } catch (e) {
    print("\nLooks like it didn't work 🙃");
    print(e);
  }

  /// Testing deserialization with generic type [int]
  final dummyJsonInt = {
    'data': [1, 2],
  };
  try {
    final testInt = GenericModel.fromJson<int>(dummyJsonInt);

    print(testInt);
    print(testInt.toJson());
  } catch (e) {
    print("\nLooks like it didn't work 🙃");
    print(e);
  }
...
```

Output:

```bash
model: GenericModel {
  data=[string 1, string 2],
}
serialized json: {data: [{$: String, : string 1}, {$: String, : string 2}]}
GenericModel {
  data=[string 1, string 2],
}
{data: [{$: String, : string 1}, {$: String, : string 2}]}
GenericModel {
  data=[1, 2],
}
{data: [{$: int, : 1}, {$: int, : 2}]}
```

It worked!  
So now we don't need to add the builder factory manually in the `serializer` object. This one-time setup is all we need.

## Going one step further <a id="futher"></a>

We've tested our `GenericModel` with `int` and `String` data types. Now let's try it with a user-defined type, a new model called `MyModel`.

We'll create a new model with two properties: name and id.

```dart
part 'my_model.g.dart';

abstract class MyModel<T> implements Built<MyModel<T>, MyModelBuilder<T>> {
  MyModel._();
  factory MyModel([void Function(MyModelBuilder<T>) updates]) = _$MyModel<T>;

  Map<String, dynamic> toJson() {
    return serializers.serializeWith(MyModel.serializer, this)
        as Map<String, dynamic>;
  }

  static MyModel fromJson(Map<String, dynamic> json) {
    return serializers.deserializeWith(MyModel.serializer, json) as MyModel;
  }

  static Serializer<MyModel> get serializer => _$myModelSerializer;

  String get name;

  String get id;
}
```

Adding this model in the `serializer.dart` file.

```dart
...
part 'serializers.g.dart';

@SerializersFor([
  GenericModel,
  MyModel, // <--- Adding new model here
])
...
```

Now, run the `dart run build_runner build` command. Now that the code is generated and the errors are gone, let's test our new model.

```dart
...
  /// Testing deserialization with generic type [MyModel]
  final dummyJsonMyModel = {
    'data': [
      {'name': 'name 1', 'id': 'id 1'},
      {'name': 'name 2', 'id': 'id 2'},
    ],
  };

  final testMyModel = GenericModel.fromJson<MyModel>(dummyJsonMyModel);

  print('deserialized model: $testMyModel');

  /// Testing serialization with generic type [MyModel]
  print('serialized json: ${testMyModel.toJson()}');
...
```

Output:

```bash
deserialized model: GenericModel {
  data=[MyModel {
    name=name 1,
    id=id 1,
  }, MyModel {
    name=name 2,
    id=id 2,
  }],
}
serialized json: {data: [{$: MyModel, name: name 1, id: id 1}, {$: MyModel, name: name 2, id: id 2}]}
```

It works. We've successfully created a custom deserializer for a collection generic type model in `build_value`.  
We also abstracted the addition of the builder factory for each generic type.

---

## Conclusion <a id="conclusion"></a>

* We have to create a custom serializer to deserialize the collection of generic data-type objects.
    
* `specifiedType` and `buildFactory` methods have to be added for all the generic types.
    
* The solution can be simplified by adding the above dependencies in the code instead of doing it manually.
    

## Final Note <a id="final-note"></a>

Thank you for reading this article. If you enjoyed it, consider sharing it with other people.  
If you find any mistakes, please let me know.  
Feel free to share your opinions below.