Erlang の JSON Schema ライブラリ jesse を使って見る
Tags: erlang
Published: 2020-01-21 02:32:22

Python の JSON Schema ライブラリ を使ったらなかなか便利だったので Erlang 版を使って見ます。

for-GET/jesse: jesse (JSon Schema Erlang) is an implementation of a JSON Schema validator for Erlang. 

- JSON Scheam ライブラリ
    - https://github.com/for-GET/jesse
- JSON ライブラリ
    - https://github.com/sile/jsone

jesse は Draft バージョン 4 まで対応しています。

まずは rebar.config の deps に jsone と jesse を指定します

```erlang
{deps,
  [
   {jsone,
    ".*", {git, "git@github.com:sile/jsone.git", {branch, "master"}}},

   {jesse,
    ".*", {git, "git@github.com:klarna/jesse.git", {branch, "master"}}}
  ]
}.
```

基本的な流れは Schema の定義をして、あとはそれに JSON を食べさせるだけです。

```erlang

%% スキーマの定義
> Schema = jsone:encode([{<<"properties">>, [{<<"a">>, [{<<"type">>, <<"integer">>}]}, {<<"b">>, [{<<"type">>, <<"string">>}]}, {<<"c">>, [{<<"type">>, <<"boolean">>}]}]}]).
<<"{\"properties\":{\"a\":{\"type\":\"integer\"},\"b\":{\"type\":\"string\"},\"c\":{\"type\":\"boolean\"}}}">>

%% バリデーション成功
> jesse:validate_with_schema(Schema, <<"{\"a\": 1, \"b\": \"b\", \"c\": true}">>, [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
{ok,[{<<"a">>,1},{<<"b">>,<<"b">>},{<<"c">>,true}]}

%% b が string ではなくて integer なのでエラーになっている
> jesse:validate_with_schema(Schema, <<"{\"a\": 1, \"b\": 1, \"c\": true}">>, [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
{error,[{data_invalid,[{<<"type">>,<<"string">>}],
                      wrong_type,1}]}
```

面倒な JSON バリデーションがこれで少しは楽になると思います。

また jesse は JSON Schema の管理機能も付いています。

```erlang

%% Schema を設定する
> Schema = jsone:encode([{<<"properties">>, [{<<"a">>, [{<<"type">>, <<"integer">>}]}, {<<"b">>, [{<<"type">>, <<"string">>}]}, {<<"c">>, [{<<"type">>, <<"boolean">>}]}]}]).
<<"{\"properties\":{\"a\":{\"type\":\"integer\"},\"b\":{\"type\":\"string\"},\"c\":{\"type\":\"boolean\"}}}">>

%% コールバック付きで Schema を登録する
> jesse:add_schema(spam, Schema, [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
ok

%% スキーマに対して処理をする
> jesse:validate(spam, <<"{\"a\": 1, \"b\": 1, \"c\": true}">>, [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
{error,[{data_invalid,[{<<"type">>,<<"string">>}],
                      wrong_type,1,
                      [<<"b">>]}]}

%% 存在しないスキーマを呼び出したときのエラー
> jesse:validate(egg, <<"{\"a\": 1, \"b\": 1, \"c\": true}">>, [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
{error,{database_error,egg,schema_not_found}}
```

この機能を使えばサーバ起動時にスキーマを全て読み込んでおき後は jesse:validate/2 を呼び出すだけになります。

## 複数の型を定義する

戻りのパターンが二種類ある場合に使う

```erlang
1> Schema = jsone:encode([{type, [<<"null">>, <<"object">>]}, {properties, [{a, [{type, <<"integer">>}, {required, true}]}]}]).
<<"{\"type\":[\"null\",\"object\"],\"properties\":{\"a\":{\"type\":\"integer\",\"required\":true}}}">>
2> jesse:validate_with_schema(Schema, jsone:encode(null), [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
src/jesse.erl:207:<0.32.0>: ParsedSchema = [{<<"type">>,[<<"null">>,<<"object">>]},
                {<<"properties">>,
                 [{<<"a">>,
                   [{<<"type">>,<<"integer">>},{<<"required">>,true}]}]}]
src/jesse.erl:208:<0.32.0>: ParsedData = null
{ok,null}
3> jesse:validate_with_schema(Schema, jsone:encode([{a, 10}]), [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
src/jesse.erl:207:<0.32.0>: ParsedSchema = [{<<"type">>,[<<"null">>,<<"object">>]},
                {<<"properties">>,
                 [{<<"a">>,
                   [{<<"type">>,<<"integer">>},{<<"required">>,true}]}]}]
src/jesse.erl:208:<0.32.0>: ParsedData = [{<<"a">>,10}]
{ok,[{<<"a">>,10}]}
4> jesse:validate_with_schema(Schema, jsone:encode([{b, 10}]), [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
src/jesse.erl:207:<0.32.0>: ParsedSchema = [{<<"type">>,[<<"null">>,<<"object">>]},
                {<<"properties">>,
                 [{<<"a">>,
                   [{<<"type">>,<<"integer">>},{<<"required">>,true}]}]}]
src/jesse.erl:208:<0.32.0>: ParsedData = [{<<"b">>,10}]
{error,[{data_invalid,[{<<"type">>,
                        [<<"null">>,<<"object">>]},
                       {<<"properties">>,
                        [{<<"a">>,
                          [{<<"type">>,<<"integer">>},{<<"required">>,true}]}]}],
                      {missing_required_property,<<"a">>},
                      [{<<"b">>,10}],
                      []}]}
5> jesse:validate_with_schema(Schema, jsone:encode([{a, <<"abc">>}]), [{parser_fun, fun(Binary) -> jsone:decode(Binary, [{format, proplist}]) end}]).
src/jesse.erl:207:<0.32.0>: ParsedSchema = [{<<"type">>,[<<"null">>,<<"object">>]},
                {<<"properties">>,
                 [{<<"a">>,
                   [{<<"type">>,<<"integer">>},{<<"required">>,true}]}]}]
src/jesse.erl:208:<0.32.0>: ParsedData = [{<<"a">>,<<"abc">>}]
{error,[{data_invalid,[{<<"type">>,<<"integer">>},
                       {<<"required">>,true}],
                      wrong_type,<<"abc">>,
                      [<<"a">>]}]}
```