Flutter-JSONのシリアライズ2

dart:convertのJsonEncoderでUint8List形式をbase64形式の文字列で出力したかったので、標準のJsonEncoderを改良してみた。

経緯

現在作成しているアプリで、SQL DBに格納されているBLOB形式のデータを、JSON形式に出力させようとしているのだけど、SQLのqueryで受け取ったmap<String, dynamic>をそのままjson.encodeに渡すと、List<int>として処理され、[1,2,3]の様な形式で出力されてしまった。

そこで、JsonEncoderが持っているtoEncodableを使いUint8Listをbase64文字列に変換して出そうとしたのだけど、うまくいかなかった。

そもそも

queryで受け取ったmap<String, dynamic>の中のUint8Listだけ初めにbase64文字列にupdateをかければいいんじゃないかと当初は考えた。

queryで受け取った後の後処理を追加しなければいけないけど。

ただ使ったパッケージがsqflite_common_ffiが悪かったのか、受け取った情報は書き込み禁止で、updateができなかった。

そのためupdateをするのではなく、queryで受け取ったものの複写を作った後にupdateをする必要があった。

次に

そこで本筋に移るのだけど、JsonEncoderにオブジェクト変換のための仕組みtoEncodableがあることが分かったので、そちらを使うことにした。変換関数は以下の様にした。

Object? toEncodableSql(dynamic object) {
  if (object is Uint8List) {
    return base64.encode(object.toList());
  }
  return object;
}

結果は変換してくれなかった。

しらべたところ、変換関数はJsonEncoderに組み込まれている変換で対応できない場合に使用されるようで、最初にtoEncodableが呼び出されるわけではないようだ。
Uint8ListはList<int>を継承しているので、組み込みのList処理がされてしまったというわけである。

このことについては、2014年にどうにかできないかとissuesにあげられていたのだけど、どうやらそのまま棚にしまわれた状態だったようだ。

Use toEncodable before trying to regularly stringify in JsonEncoder · Issue #21251 · dart-lang/sdk
This issue was originally filed by @stevenroose What steps will clearly show the issue / need for enhancement? Create a ...

2019年に再燃したようだけど、やっぱり棚上げされてしまっている。

対応

この件、需要が無いのか、他の対策があるのかわからないのだが、あまり情報がなかった。

そこで、dart:convertを改良しtoEncodableを標準の処理より前に動作するように改良してみた。

そのソースがこちら。

A slightly improved version of the Dart standard Json Encoder.
A slightly improved version of the Dart standard Json Encoder. - tiny_json_encoder_base.dart

今回の改良でtoEncodableの仕様を変更する必要があった。

標準のものは、Object? Function(dynamic object)なんだけど、bool Function(dynamic object, List<Object?>)に変更したこと。

toEncodableでオブジェクトの変換をする部分の都合上、変換できたかどうかを返すboolと変換結果の2つを返す必要があった。

returnを[bool, Object]のtuppleモドキにするのが良いかなと思ったのだけど、なんとなく上の様な返り値を第2引数に入れる形にして見た。

またdecode処理で使われるreviverモドキを入れられるようにした。

呼び出し方法は以下の様な感じになる。

import 'package:tiny_json_encoder/tiny_json_encoder.dart';
import 'dart:convert';
import 'dart:typed_data';

/// Conversion function using JsonStringStringifier.
String converter(Object? object) {
  var output = StringBuffer();
  JsonStringStringifier(
      sink: output,
      toEncodable: (obj, output) {
        if (obj is DateTime) {
          output[0] = obj.millisecondsSinceEpoch;
          return true;
        } else if (obj is Uint8List) {
          output[0] = base64.encode(obj.toList());
          return true;
        }
        return false;
      },
      keyConverter: (key, object) {
        if (key == "2") {
          return (object as DateTime).toIso8601String();
        }
        return object;
      }).writeObject(object);
  return output.toString();
}

void main() {
  final encoder = JsonEncoder(convert: converter);
  final val = Uint8List.fromList("abcdefg".codeUnits);
  print(encoder.convert({
    "1": [1, 2, 3, DateTime.parse("20220101"), val],
    "2": DateTime.parse("20200202"),
  }));
}

結果は以下の様になる。

{"1":[1,2,3,1640962800000,"YWJjZGVmZw=="],"2":"2020-02-02T00:00:00.000"}

最後に

標準のものも、変換で使われているクラスをプライベートクラスではなくパブリッククラスにしてくれてたらその派生を使って個別に作れたのにと思うのだけど、JsonEncoderから呼ばれる仕組みが独自だったから公開したくなかったのかな。

普通に考えるなら、JsonEncoderに変換クラスを紐づけてそれに処理をさせるといった感じで使うだろうと思うのだけど、JsonEncoder.convertで、毎回オブジェクトを作ってそれを使って変換させるといったことを行っていた。

変換オブジェクトの状態を初期状態で使いたいがために、そうしていたのかな。
ちょっと不思議な実装だった。

後、このソースはローカル環境ではパッケージ化してみたのだけど、公開の方法がちょっと不明なので、gistでのソース公開だけにしておいた。

コメント

タイトルとURLをコピーしました