Skip to content

gumi/simple_schema

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 == 20

Group スキーマを defschema/1 で定義していますが、:group_name が文字列であり、:personsPerson の配列であることが、見ればすぐに分かるでしょう。

これによって、共通するスキーマに名前を付けて再利用することができます。

シンプルスキーマ

もう少し詳細に 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
end

DateTimeSchemaSimpleSchema ビヘイビアを実装しているためシンプルスキーマになります。 そのため、以下のように 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 を任意の型に変換して返します。 valueschema/1 で返したシンプルスキーマによる検証が済んでいて、例えば上記の DateTimeSchema.from_json/3 に渡された value は、{:string, format: :datetime} で検証されています。 そのため value が文字列であり、:datetime のフォーマットであることが保証されています。

ただし、SimpleSchema.from_json/2optimistic: 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
end

schema/1 で、:name:age を持つマップとしてシンプルスキーマを定義しています。 渡された JSON のオブジェクトがこのシンプルスキーマの構造になっているかを検証した後、Person.from_json/3 を呼び出して、valuePerson 構造体に変換しています。 JSON のオブジェクトを特定の構造体に変換するためのヘルパーとして SimpleSchema.Type.json_to_struct/3 があるので、これを使うと簡単に変換できます。

About

Validate JSON and store to a specified data structure

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages