type holyshared = Engineer<mixed>

PHP、Hack、Ruby、OCaml、Rust、Javascript周りの技術ブログ

HackでのJSONのパース

HackでJSONをパースする際にjson_decodeを使うと思うのですが、JSON_FB_HACK_ARRAYSが新しく指定できるようになったみたいなので調べて見ました。

また、JSONを扱う場合の実装をどうするかも検討しました。

json_decodeのオプションによる型の違い

オプションなし

オプションを指定しない場合、パースした結果はarray型になるようです。

function test1(): array<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true);
  var_dump($v);
  return $v;
}

出力

array(2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  array(1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

JSON_FB_COLLECTIONS

このオプションの場合、パースした結果はMap型になるようです。

// JSON_FB_COLLECTIONS
function test2(): Map<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true, 512, JSON_FB_COLLECTIONS);
  var_dump($v);
  return $v;
}

出力

object(HH\Map)#1 (2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  object(HH\Map)#2 (1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

JSON_FB_HACK_ARRAYS

このオプションの場合、パースした結果はdict型になるようです。
JSONの構造が静的解析時にわからないので、dict<string, mixed>扱いになりそうです。

// JSON_FB_HACK_ARRAYS
function test3(): dict<string, mixed> {
  $j = file_get_contents("test.json");
  $v = json_decode($j, true, 512, JSON_FB_HACK_ARRAYS);
  var_dump($v);
  return $v;
}

出力

dict(2) {
  ["name"]=>
  string(3) "Bob"
  ["group"]=>
  dict(1) {
    ["name"]=>
    string(9) "HHVM/Hack"
  }
}

通常はどうするか

普通にJSONを扱うコードを書く分には、次のようにするのをお勧めします。

  1. 構造の表現にshapeを使用する
  2. type-assertを使用する

1. 構造の表現にshapeを使用する

dict、array、Mapで型指定してしまうと、どうしてもmixedを使用する羽目になるので、タイプチェッカーと相性が悪くなります。
mixedはnullを含む、全てのものが値として考えられるので、極力使うべきではないと思います。 代わりにJSONのフォーマットをshapeで表現します。

/**
 * 次のようなJSONの場合
 *
 * {
 *   "name": "Bob",
 *   "group": {
 *     "name": "HHVM/Hack"
 *   }
 * }
 */

type GroupJSON = shape(
  "name" => string
);

type UserJSON = shape(
  "name" => string,
  "group" => GroupJSON
);

shapeにした場合、定義されていないフィードに対してのアクセスは静的解析時に検出することができます。
パースした結果を参照したりするコードに非常に有効です。

2. type-assertを使用する

type-assertパッケージを使用して、実行時にチェックを行うようにします。 https://github.com/hhvm/type-assert

パースするJSONをファイルから読み込んだりする場合、期待する構造のものかは実行時にしか基本的にはわからないため、静的解析時の問題検出は諦めて、実行時に検出する方針です。

TypeAssert\matches_type_structureTypeStructure<T>と検査対象の値を受け取り、値が型の定義を満たしているか検証します。
値が仕様を満たしている場合は、Tの値をそのまま返します。

TypeStructure<T>を得るにはtype_structure関数を使用します。
第1引数にclass名、またはオブジェクト、第2引数にはType Constantの名前を指定します。

この例では、JSON形式のサーバーの設定を読み込む例です。

function server_config_from_json(string $path): ServerConfiguration {
  $content = file_get_contents($path);
  $rawConfig = json_decode($content, true, 512, JSON_FB_HACK_ARRAYS);
  $validConfig = TypeAssert\matches_type_structure(
    type_structure(ServerConfiguration::class, 'T'),
    $rawConfig
  );
  return new ServerConfiguration($validConfig);
}

ServerConfigurationの定義はこうなっています。

//JSONの設定ファイル
interface JSONConfiguration {
  abstract const type T; //ここの型はインターフェースを実装するクラスで指定する
}

final class ServerConfiguration implements JSONConfiguration {
  //JSONのスキーマを指定する
  const type T = shape(
    "host" => string,
    "port" => int
  );

  public function __construct(private this::T $json) : void
  {
  }

  public function host() : string {
    return $this->json['host'];
  }

  public function port() : int {
    return $this->json['port'];
  }
}

困りごと

TypeStructure<T>は基本的にtype_structure関数でしか、生成できない見たいです。
ReflectionTypeAliasのgetTypeStructureメソッドでいけないかなと思ったのですが、hhiファイルの定義上はarrayを返すようになっていて、 TypeStructure<T>ではないとタイプチェックでエラーになりました。

TypeStructure<T>はshapeなので、shapeを生成するコードを書くしかないぽいです。