View Source SimpleSchema behaviour (simple_schema v1.2.3)
SimpleSchema
SimpleSchema は JSON の検証と各データ構造への設定を行うライブラリです。
動機
HTTP の API サーバを書いていると、よく HTTP の POST リクエストで JSON を受け取ることがありますが、この値が正しいフォーマットかどうかを検証したいことがあります。 検証だけなら、JSON Schema を使うという手があり、幸いなことに Elixir には JSON Schema を実装した ExJsonSchema というライブラリがあります。
しかし、JSON Schema は手で書くのが大変です。もう少し機能を絞った単純なスキーマを使いたいところです。
また、JSON Schema は検証しか行わないため、データにアクセスするのに手間が掛かります。
json = Poison.decode!(conn.body_param)
:ok = validate(json)
hp = json["player"]["hp"]
# → json.player.hp と書きたい
datetime = json["datetime"] # 文字列を取り出して
{:ok, datetime, _} = DateTime.from_iso8601(value) # DateTime 型に変換する
# → json.datetime した段階で DateTime 型であって欲しいこのように、特に DateTime に変換するといった処理が必要な場合、検証と同時に変換まで済ませたいのです。
そこで、簡単にスキーマを書けるようして、検証を行い、それらのデータを変換するライブラリとして、SimpleSchema というライブラリを作りました。
使い方
以下のように使います。
# defschema/1 を使ってスキーマを定義する
defmodule Person do
import SimpleSchema, only: [defschema: 1]
defschema [
name: :string,
age: {:integer, minimum: 0},
]
end
# JSON 文字列をデコードしたデータを…
json = %{
"name" => "John Smith",
"age" => 42,
}
# Person と一緒に from_json!/2 すると、Person 構造体に値が設定される
person = SimpleSchema.from_json!(Person, json)
assert person.name == "John Smith"
assert person.age == 42このように、defschema/1 でスキーマを定義して、SimpleSchema.from_json!/2 にそのスキーマと JSON オブジェクトを渡すと、JSON オブジェクトを検証し、指定したスキーマにデータを入れてくれます。
JSON オブジェクトが Person スキーマを満たしていない場合、以下のようにエラーがでます。
bad_json = %{
"name" => 100, # 文字列ではない
"age" => -10, # 無効な年齢
"__additional_key__" => 0, # 余分なキー
}
# from_json/2 は失敗する
{:error, reason} = SimpleSchema.from_json(Person, bad_json)
IO.inspect reason出力:
[{"Expected the value to be >= 0", "#/age"},
{"Type mismatch. Expected String but got Integer.", "#/name"},
{"Schema does not allow additional properties.", "#/__additional_key__"}]この Person を内包するスキーマを定義することもできます。
つまりスキーマはネスト可能です。
# Person を内包する Group
defmodule Group do
import SimpleSchema, only: [defschema: 1]
defschema [
group_name: :string,
persons: [Person],
]
end
json = %{
"group_name" => "A Group",
"persons" => [%{
"name" => "John Smith",
"age" => 42,
}, %{
"name" => "YAMADA Taro",
"age" => 20,
}],
}
group = SimpleSchema.from_json!(Group, json)
assert group.group_name == "A Group"
assert Enum.fetch!(group.persons, 1).age == 20Group スキーマを defschema/1 で定義していますが、:group_name が文字列であり、:persons が Person の配列であることが、見ればすぐに分かるでしょう。
これによって、共通するスキーマに名前を付けて再利用することができます。
シンプルスキーマ
もう少し詳細に SimpleSchema の機能を説明します。
SimpleSchema ライブラリが定義している、SimpleSchema.from_json/2 の第1引数に渡せるスキーマのことを シンプルスキーマ と呼びます。
JSON Schema と比べると大分単純で直感的な構文になっているので「シンプル」と名付けています。
例えば、:integer はシンプルスキーマです。
value = SimpleSchema.from_json!(:integer, 10)
assert value == 10:integer シンプルスキーマは、渡された値が整数であるかを確認し、整数であればその値を戻り値にします。
整数に制約を付け加えることも可能です。
value = SimpleSchema.from_json!({:integer, minimum: 10, maximum: 20}, 5)
# RuntimeError: [{"Expected the value to be >= 10", "#"}]{:integer, opts} という書き方もシンプルスキーマになります。
これは渡された値が整数であり、かつ10から20の範囲内であるかを確認し、正しければその値を戻り値にします。
%{} という書き方もシンプルスキーマであり、各フィールドには、更にシンプルスキーマを渡すことができます。
schema = %{
value: {:integer, optional: true},
point: %{
x: :integer,
y: :integer,
},
}
data = %{
"point" => %{
"x" => 10,
"y" => 20,
}
}
value = SimpleSchema.from_json!(schema, data)
# value == %{point: %{x: 10, y: 20}}
assert value.point.x == 10
assert value.point.y == 20このシンプルスキーマは、渡された値がマップであるかを確認し、渡された値の各フィールドが、指定したシンプルスキーマのフィールドと合っているかどうか確認します。 正しければ、渡されたマップのキーを atom にした上で戻り値にします。
また、:value フィールドに optional: true という制約を付与しました。
これはマップのフィールドに渡すシンプルスキーマのみに指定可能で「このフィールドが無くてもエラーにしない」という意味になります。
そのため data に "value" キーが存在していなくても SimpleSchema.from_json!/2 が成功しています。
シンプルスキーマの一覧
シンプルスキーマは、以下のいずれかである必要があります。
:booleanまたは{:boolean, opts}:integerまたは{:integer, opts}:numberまたは{:number, opts}:nullまたは{:null, opts}:stringまたは{:string, opts}:anyまたは{:any, opts}%{...}または{%{...}, opts}[...]または{[...], opts}SimpleSchemaビヘイビアを実装したモジュール、または{Module, opts}
opts には各制約をキーワードリストで指定します。
制約の一覧
制約の一覧は以下の通りです。
{:nullable, boolean}: もしtrueならnilを許可する。:null以外のシンプルスキーマに指定可能。{:minimum, integer}: 最小値。:integerと:numberに指定可能。{:maximum, integer}: 最大値。:integerと:numberに指定可能。{:min_items, non_neg_integer}: 最小の要素数。:arrayに指定可能。{:max_items, non_neg_integer}: 最大の要素数。:arrayに指定可能。{:unique_items, boolean}: もしtrueなら配列がユニークであることを要求される。:arrayに指定可能。{:min_length, non_neg_integer}: 最小の長さ。:stringに指定可能。{:max_length, non_neg_integer}: 最大の長さ。:stringに指定可能。{:enum, [...]}: 要素に指定可能な値のリスト。:integerと:stringに指定可能。{:format, :datetime | :email}: 事前に定義されたフォーマットで検証する。:stringに指定可能。{:optional, boolean}: もしtrueなら、%{...}の子要素として必須では無い。%{...}の子要素のみ指定可能。{:tolerant, boolean}: もしtrueなら、生成される JSON Schema に"additionalProperties"が設定される。つまり子要素に指定されてない要素を許可するようになる。%{...}に指定可能。デフォルトはfalse。{:default, any}: フィールドのデフォルト値。渡された JSON にこのフィールドが存在しなかった場合はこの値になる。%{...}の子要素のみ指定可能。{:field, string}: 対応する JSON のフィールド名。%{...}の子要素のみ指定可能。
メタ情報
シンプルスキーマや制約以外の情報は opts に :meta キーを使って追加します。
schema = %{
value: {:integer, optional: true},
point: %{
x: {:integer, meta: %{description: "x座標"}},
y: {:integer, meta: %{description: "y座標"}},
},
}
SimpleSchema ビヘイビア
SimpleSchema ビヘイビアを実装したモジュールは、シンプルスキーマになります。
これを使うことで、特定のスキーマに名前を付けたり、特定の構造体に変換できるようになります。
例えば、"2017-11-27T11:49:50+09:00" といった ISO 8601 に従った日付を DateTime 型として取得するには、以下のように定義します。
defmodule DateTimeSchema do
@behaviour SimpleSchema
@impl SimpleSchema
def schema(_opts) do
{:string, format: :datetime}
end
@impl SimpleSchema
def from_json(_schema, value, _opts) do
case DateTime.from_iso8601(value) do
{:ok, datetime, _} -> {:ok, datetime}
{:error, reason} -> {:error, reason}
end
end
@impl SimpleSchema
def to_json(_schema, value, _opts) do
{:ok, DateTime.to_iso8601(value)}
end
endDateTimeSchema は SimpleSchema ビヘイビアを実装しているためシンプルスキーマになります。
そのため、以下のように SimpleSchema.from_json!/2 に渡すことができます。
datetime = SimpleSchema.from_json!(DateTimeSchema, "2017-11-27T11:49:50+09:00")
# datetime == #DateTime<2017-11-27 02:49:50Z>このように SimpleSchema ビヘイビアを実装することで、特定のスキーマに DateTimeSchema というを付け、DateTime 型の構造体に変換して利用できるようになります。
なお、上記の DateTimeSchema に相当する機能は既に SimpleSchema.Type.DateTime に入っています。
SimpleSchema ビヘイビアが要求する関数は、以下の通りです。
@callback schema(opts :: Keyword.t) :: simple_schema
@callback from_json(schema :: simple_schema, json :: any, opts :: Keyword.t) :: {:ok, any} | {:error, any}
@callback to_json(schema :: simple_schema, value :: any, opts :: Keyword.t) :: {:ok, any} | {:error, any}schema/1 で、そのモジュールが要求するシンプルスキーマを定義します。
from_json/3 で、value を任意の型に変換して返します。
value は schema/1 で返したシンプルスキーマによる検証が済んでいて、例えば上記の DateTimeSchema.from_json/3 に渡された value は、{:string, format: :datetime} で検証されています。
そのため value が文字列であり、:datetime のフォーマットであることが保証されています。
ただし、SimpleSchema.from_json/2 に optimistic: true が指定されていた場合、検証を行いません。この場合、正しい値を渡す責任はユーザにあります。
to_json/3 で、変換された値をシンプルスキーマの満たす文字列に変換します。
from_json/3 と逆の変換を行います。
この関数は SimpleSchema.to_json/2 の内部で利用される関数なので、不要であれば常に {:error, "not implemented"} でも構いません。
defschema/1
defschema/1 は、defstruct/1 による構造体の定義と、SimpleSchema ビヘイビアの実装を行います。
defmodule Person do
import SimpleSchema, only: [defschema: 1]
defschema [
name: :string,
age: {:integer, minimum: 0},
]
endこのコードは、以下の様に変換されます。
defmodule Person do
@enforce_keys [:name, :age]
defstruct [:name, :age]
@behaviour SimpleSchema
@impl SimpleSchema
def schema(_opts) do
%{
name: :string,
age: {:integer, minimum: 0},
}
end
@impl SimpleSchema
def from_json(schema, value, _opts) do
SimpleSchema.Type.json_to_struct(__MODULE__, schema, value)
end
@impl SimpleSchema
def to_json(schema, value, _opts) do
SimpleSchema.Type.struct_to_json(__MODULE__, schema, value)
end
endschema/1 で、:name と :age を持つマップとしてシンプルスキーマを定義しています。
渡された JSON のオブジェクトがこのシンプルスキーマの構造になっているかを検証した後、Person.from_json/3 を呼び出して、value を Person 構造体に変換しています。
JSON のオブジェクトを特定の構造体に変換するためのヘルパーとして SimpleSchema.Type.json_to_struct/3 があるので、これを使うと簡単に変換できます。
Summary
Functions
Generate a struct and implement SimpleSchema behaviour by the specified schema.
Convert JSON value to a simple schema value.
Convert a simple schema value to JSON value.
Types
@type simple_schema() :: SimpleSchema.Schema.simple_schema()
Callbacks
@callback from_json(schema :: simple_schema(), json :: any(), opts :: Keyword.t()) :: {:ok, any()} | {:error, any()}
@callback schema(opts :: Keyword.t()) :: simple_schema()
@callback to_json(schema :: simple_schema(), value :: any(), opts :: Keyword.t()) :: {:ok, any()} | {:error, any()}
Functions
Generate a struct and implement SimpleSchema behaviour by the specified schema.
defmodule MySchema do
defschema [
username: {:string, min_length: 4},
email: {:string, default: "", optional: true, format: :email},
]
endis converted to:
defmodule MySchema do
@enforce_keys [:username]
defstruct [:username, email: ""]
@behaviour SimpleSchema
@simple_schema %{
username: {:string, min_length: 4},
email: {:string, default: "", optional: true, format: :email},
}
@impl SimpleSchema
def schema(opts) do
{@simple_schema, opts}
end
@impl SimpleSchema
def from_json(schema, value, _opts) do
SimpleSchema.Type.json_to_struct(__MODULE__, schema, value)
end
@impl SimpleSchema
def to_json(schema, value, _opts) do
SimpleSchema.Type.struct_to_json(__MODULE__, schema, value)
end
end
Convert JSON value to a simple schema value.
JSON value is validated before it is converted to a simple schema value.
If optimistic: true is specified in opts, JSON value is not validated before it is converted.
Convert a simple schema value to JSON value.
If optimistic: true is specified in opts, JSON value is not validated after it is converted.
Otherwise, JSON value is validated after it is converted.