How to override or pipe a nested builder with built_value?

38 views Asked by At

I have an API which sends me a type :

type Response = {
  id: string;
  values: Stringified<number[]>, // string
}

where type Stringified<T> = string; and is what you would get by using jsonEncode.

I want to create a Built class that matches the response architecture. This is what I have for now:

@SerializersFor([
  MyResponse,
])
final Serializers _serializers = (_$_serializers.toBuilder()
      ..addPlugin(StandardJsonPlugin())
    .build();

abstract class MyResponse implements Built<MyResponse, MyResponseBuilder> {
  factory MyResponse([void Function(MyResponseBuilder) updates]) = _$MyResponse;

  MyResponse._();

  Map<String, dynamic> toJson() {
    return _serializers.serializeWith(MyResponse.serializer, this)! as Map<String, dynamic>;
  }

  static MyResponse fromJson(Map<String, dynamic> json) {
    return _serializers.deserializeWith(MyResponse.serializer, json)!;
  }

  static Serializer<MyResponse> get serializer => _$myResponseSerializer;

  String get id;

  List<int> get values;
}

This would work if I was receiving/sending data like :

{ "id": "id", "values": [0, 1, 2] }

But the data I receive or need to send is:

{ "id": "id", "values": "[0, 1, 2]" }

How can I override the toJson / fromJson of values or how can I add a pipe/how so I can use jsonEncode/jsonDecode when exporting to/importing from a JSON?

2

There are 2 answers

0
Valentin Vignal On BEST ANSWER

In the end, I managed to do it with a SerializerPlugin :

Here is a more complete example with 2 models:

type Model1 {
  boolean: boolean;
  integer: number;
}

type Model2 = {
  char: string;
  list: Stringified<Model1[]>;
}

where type Stringified<T> = string; and is what you would get by using jsonEncode.

I created those built value classes:

// model_1.dart

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

part 'model_1.g.dart';

@SerializersFor([
  Model1,
])
final Serializers _serializers =
    (_$_serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

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

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

  static Model1 fromJson(Map<String, dynamic> json) {
    return _serializers.deserializeWith(Model1.serializer, json)!;
  }

  static Serializer<Model1> get serializer => _$model1Serializer;

  bool get boolean;

  int get integer;
}
// model_2.dart

import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:flutter_app_stable/model_1.dart';
import 'package:flutter_app_stable/my_plugin.dart';

part 'model_2.g.dart';

@SerializersFor([
  Model2,
  Model1,
])
final Serializers _serializers = (_$_serializers.toBuilder()
      ..addPlugin(StandardJsonPlugin())
      ..addPlugin(MyPlugin()) // <- Add the plugin here.
      ..addBuilderFactory(
        const FullType(BuiltList, [FullType(Model1)]),
        ListBuilder<Model1>.new,
      ))
    .build();

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

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

  static Model2 fromJson(Map<String, dynamic> json) {
    return _serializers.deserializeWith(Model2.serializer, json)!;
  }

  static Serializer<Model2> get serializer => _$model2Serializer;

  String get char;

  BuiltList<Model1> get list;
}
// my_plugin.dart

class MyPlugin extends SerializerPlugin {
  @override
  Object? afterDeserialize(Object? object, FullType specifiedType) {
    return object;
  }

  @override
  Object? afterSerialize(Object? object, FullType specifiedType) {
    if (specifiedType.root == BuiltList &&
        specifiedType.parameters.length == 1 &&
        specifiedType.parameters.first.root == Model1) {
      return jsonEncode(object);
    } else {
      return object;
    }
  }

  @override
  Object? beforeDeserialize(Object? object, FullType specifiedType) {
    if (specifiedType.root == BuiltList &&
        specifiedType.parameters.length == 1 &&
        specifiedType.parameters.first.root == Model1) {
      return jsonDecode(object as String);
    } else {
      return object;
    }
  }

  @override
  Object? beforeSerialize(Object? object, FullType specifiedType) {
    return object;
  }
}

Notice the class MyPlugin extends SerializerPlugin in model_2.dart. I add this plugin to the Model2 serializer.

It

  • Overrides afterSerialize that applies jsonEncode to the serialized list of serialized model1
  • Overrides beforeDeserialize that applies jsonDecode to the deserialized stringified list of serialized model1
1
pmatatias On

Im not sure this fit to your need. I ussually modify my toJson() method by changet its value.

for example: in my model I use @JsonSerializable which will generated default toJson.to customize some keys, i do like below:

Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = _$MyModelToJson(this);
    data['status'] = status?.id;
    return data;
  }

maybe you can use similar pattern.

Map<String, dynamic> toJson() {
   final  data = _serializers.serializeWith(MyResponse.serializer, this)! as Map<String, dynamic>;

   data['values'] = "${data['values']}";

   return data;
  }