ppx_yojson_conv で Web サービスの API を叩くときのコツ
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_yojson
や t_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 を使ってレコードを定義しなければなりません。
k2 : int option
を使う
失敗 1: ppx_yojson_conv では option
を扱うことができます。まずこれを使って次のようにレコードを定義してみます:
type t1 = {
k1 : string;
k2 : int option;
}
[@@deriving yojson]
残念ながらこの定義では、例 1 と 例 2 を扱うことはできるのですが、例 3 は扱うことができません。具体的には 例 2 のデシリアライズに失敗します。
k2 : int option [@yojson.option]
失敗 2: 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;
()
-
似たようなライブラリに ppx_deriving_yojson もありますが、README によると、こちらは新規採用はしないほうが良いそうです。 ↩︎
Discussion