🙌

ppx_yojson_conv で Web サービスの API を叩くときのコツ

2024/04/15に公開

OCaml でよく使われる JSON ライブラリに yojson があります。これは以下のようなデータを:

`Assoc [
  ("hello", `List [ `String "world" ]);
]

以下のような JSON データに変換したり、逆変換したりしてくれます:

{
  "hello": ["world"]
}

この yojson を使ってレコードをシリアライズしたりデシリアライズしたりする際には ppx_yojson_conv が便利です。例えば次のようなレコードを定義しておくと:

type t = {
  hello : string list;
}
[@@deriving yojson]

ppx_yojson_conv によって勝手に t_of_yojsont_to_yojson のような関数が定義され、これらを呼び出すことで t の JSON へのシリアライズやデシリアライズを行うことができます[1]

そんな感じで大変便利な ppx_yojson_conv なのですが、これを使ってシリアライズした JSON を適当な Web サービスの API に投げつけたり、API から返却されたりする JSON をデシリアライズしようと思うと、少し面倒なことになります。例えば次のような(よくある)API を考えてみます:

  • フィールド k1 の値は文字列で、必須
  • フィールド k2 の値は数値で、任意

この API では次のような JSON を受け渡すことになるでしょう:

{"k1": "hello", "k2": 10}   // 例1. どちらのフィールドも埋まっているパターン
{"k1": "hello", "k2": null} // 例2. k2 には null が入っているパターン
{"k1": "hello"}             // 例3. k2 が省略されるパターン

上のすべてのパターンを正しくハンドルするように ppx_yojson_conv を使ってレコードを定義しなければなりません。

失敗 1: k2 : int option を使う

ppx_yojson_conv では option を扱うことができます。まずこれを使って次のようにレコードを定義してみます:

type t1 = {
    k1 : string;
    k2 : int option;
}
[@@deriving yojson]

残念ながらこの定義では、例 1 と 例 2 を扱うことはできるのですが、例 3 は扱うことができません。具体的には 例 2 のデシリアライズに失敗します。

失敗 2: k2 : int option [@yojson.option]

ppx_yojson_conv の README をよく読むと [@yojson.option] というアトリビュートを使えることが分かります。これを使えば option のフィールドがない場合に対応できそうです。これを使ってみます:

type t2 = {
    k1 : string;
    k2 : int option; [@yojson.option]
}
[@@deriving yojson]

この定義では 例 1 と 例 3 は扱えるのですが、今度は 例 2 をデシリアライズできなくなってしまいます。

正解: k2 : int option [@yojson.default None]

というわけで正解がこちらです。デフォルト値を指定する yojson.default を使い、None をデフォルト値にしておきます。次のようにレコードを定義します:

type t0 = {
    k1 : string;
    k2 : int option; [@yojson.default None]
}
[@@deriving yojson]

これで 1, 2, 3 全ての例をデシリアライズできます。int option[@yojson.option] 抜きで使うことで例 1 と 2 に対応しつつ、例 3 の場合はデフォルト値が使われるのでエラーになりません。

ちなみに、この定義を使って k2 の値が None の場合をシリアライズすると、k2 のフィールドには null が入ります。k2 を省略した JSON を出力したい場合は [@yojson_drop_default (=)] を追加で書いておきます。

まとめ

ppx_yojson_conv 便利なのでみんな使ってください。

付録

以下のコードで実験できます:

open Ppx_yojson_conv_lib.Yojson_conv

type t1 = { k1 : string; k2 : int option } [@@deriving yojson]
type t2 = { k1 : string; k2 : int option [@yojson.option] } [@@deriving yojson]

type t0 = { k1 : string; k2 : int option [@yojson.default None] }
[@@deriving yojson]

type t0' = {
  k1 : string;
  k2 : int option; [@yojson.default None] [@yojson_drop_default ( = )]
}
[@@deriving yojson]

let () =
  let v1 = `Assoc [ ("k1", `String "hello"); ("k2", `Int 10) ] in
  let v2 = `Assoc [ ("k1", `String "hello"); ("k2", `Null) ] in
  let v3 = `Assoc [ ("k1", `String "hello") ] in

  let _ = t1_of_yojson v1 in
  let _ = t1_of_yojson v2 in
  (*let _ = t1_of_yojson v3 in*)
  let _ = t2_of_yojson v1 in
  (*let _ = t2_of_yojson v2 in*)
  let _ = t2_of_yojson v3 in
  let _ = t0_of_yojson v1 in
  let _ = t0_of_yojson v2 in
  let _ = t0_of_yojson v3 in

  v3 |> t0_of_yojson |> yojson_of_t0 |> Yojson.Safe.to_string |> print_endline;
  v3 |> t0'_of_yojson |> yojson_of_t0' |> Yojson.Safe.to_string |> print_endline;

  ()
脚注
  1. 似たようなライブラリに ppx_deriving_yojson もありますが、README によると、こちらは新規採用はしないほうが良いそうです。 ↩︎

Discussion