JSON
TypeScript

TypeScriptのDecoratorを使ってserializableを実現する

More than 1 year has passed since last update.

JavaのSerializableのようなものをJavaScriptで!
デコレータ使ってJSONのstringify/parseでオブジェクトを簡単に復元できるようにしてみる。

toJSON

前知識。
JavaScriptの仕様として、オブジェクトにtoJSONを実装しておくとJSON.stringifyの時に参照してくれる。
別のオブジェクト内部にあってもOK。

var obj = {
  a: 1,
  b: {
    c: 1,
    toJSON: function(){ return 'hoge'; }
  }
};

JSON.stringify(obj); // => "{"a":1,"b":"hoge"}"

これオライリーのJavaScript本読むまで知らなかった。みんなちゃんと定義書読もうね。

parse

また、JSON.parseにも第二引数に関数を設定して処理を挟める。

JSON.parse('{"a":1,"b":2}', function(key,value) {
  if(key === 'b') {
    return 'piyo';
  } else {
    return value;
  }
}); // => Object {a: 1, b: "piyo"}

前知識ここまで。

Decoratorでオブジェクト復元

TypeScriptのDecorator使ったらオブジェクト簡単に復元できんじゃね?ってことで、
Decorator使って、クラスにtoJSONを生やすものを書いてみた。
追記: コンストラクタの格納方法を修正。

serializable.ts
const constructors = {}; // コンストラクタ格納

// class用decorator (toJSONを生やす)
export const serializable = (target: any) => {
  constructors[target.name] = target;
  target.prototype.toJSON = function(key) {
    if (key === '__serializeValue') { // 入れ子ループ防止用
      return this;
    } else {
      // デシリアライズに必要な情報を埋める
      return {
        __serializeName: target.name,
        __serializeValue: this
      };
    }
  };
};

// JSON.parse用関数
export const deserialize = (k:string, v:any) => {
  // 関係ないオブジェクトは通常処理
  if(!v.__serializeName) return v;

  if (constructors[v.__serializeName].fromJSON){
    // fromJSONが定義されていればそれを利用
    return constructors[v.__serializeName].fromJSON(v.__serializeValue);
  } else {
    // 定義されていなければ無引数でコンストラクタ実行して値設定
    const obj = new constructors[v.__serializeName]();
    for (let key in v.__serializeValue) {
      if (v.__serializeValue.hasOwnProperty(key)) {
        obj[key] = v.__serializeValue[key];
      }
    }
    return obj;
  }
};

こうやって使う。

import {serializable, deserialize} from './serializable'

@serializable // シリアライズ可能にする!
class Hoge{
  piyo: number;
  fuga: number;
  piyofuga() {
    return this.piyo + this.fuga;
  }
}

const hoge = new Hoge();
hoge.piyo = 3;
hoge.fuga = 2;
console.log(hoge.piyofuga()); // => 5

// 文字列化
const json = JSON.stringify(hoge);
console.log(json);
// => {"__serializeName":"Hoge","__serializeValue":{"piyo":3,"fuga":2}}

// 復元
const obj = JSON.parse(json, deserialize);
console.log(obj.piyofuga()); // => 5

ただしこの場合、無引数でコンストラクタが実行されてしまう。
なので、静的関数fromJSONがあればそれを使うようにしてある。

import {serializable, deserialize} from './serializable'

@serializable
class Hoge{
  constructor(piyo: number, fuga:number) {
    this.piyo = piyo;
    this.fuga = fuga;
    console.log(`Created! piyo:${piyo} fuga:${fuga}`);
  }
  // JSONからの復元方法を指定
  static fromJSON(json: {piyo: number, fuga:number}):Hoge {
    return new Hoge(json.piyo, json.fuga);
  }
}

const hoge = new Hoge(3, 2); // => "Created! piyo:3 fuga:2"
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:3 fuga:2"

// fromJSONがない場合
delete Hoge.fromJSON;
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:undefined fuga:undefined"

ちなみに、別段Decorator使わなくても普通に呼んでもいい。
むしろこっちのほうが汎用的かもしれない。(ええ...)

class Hoge{
  piyo: number;
  fuga: number;
  piyofuga() {
    return this.piyo + this.fuga;
  }
}

serializable(Hoge); // toJSON生やす

状態保存に使えたりしないかな。