💎️

Ruby で使える JSON Schema ライブラリを試す

2024/08/04に公開

調べたもの

ライブラリ 説明 スター数
json-schema 高性能 1.5k
json_schemer 速い 391
dry-schema Schema を Ruby で書く 412
jtd JSON Schema ではないっぽい 10

用意したデータ

json_schema = {
  "type" => "object",
  "properties" => {
    "name" => {"type" => "string"},
    "age" => {"type" => "integer", "minimum" => 0},
  },
  "required" => ["name", "age"],
}
ok_data = { "name" => "alice", "age" => 20 }
ng_data = { "name" => "alice", "age" => -1 }

json-schema gem

require "json-schema"

判定を真偽で受け取るなら、

JSON::Validator.validate(json_schema, ok_data)  # => true
JSON::Validator.validate(json_schema, ng_data)  # => false

例外を出すには、

JSON::Validator.validate!(json_schema, ok_data) rescue $!  # => true
JSON::Validator.validate!(json_schema, ng_data) rescue $!  # => #<JSON::Schema::ValidationError: The property '#/age' did not have a minimum value of 0, inclusively in schema 3a1266ff-f235-5379-8cf6-d4f88d9bb20a>

! をつけるルールなのはわかりやすい。

エラー文言が必要なときは、

errors = JSON::Validator.validate(json_schema, ng_data, record_errors: true)
errors  # => ["The property '#/age' did not have a minimum value of 0, inclusively in schema 3a1266ff-f235-5379-8cf6-d4f88d9bb20a"]

とする。

json_schemer gem

require "json_schemer"

まず JSON Schema 自体の表記チェックができる。

JSONSchemer.valid_schema?(json_schema)  # => true

使うには、いったん JSONSchemer::Schema 型にして、

schemer = JSONSchemer.schema(json_schema)  # => #<JSONSchemer::Schema @value={"type"=>"object", "properties"=>{"name"=>{"type"=>"string"}, "age"=>{"type"=>"integer", "minimum"=>0}}, "required"=>["name", "age"]} @parent=nil @keyword=nil>

真偽で受け取るなら、

schemer.valid?(ok_data)  # => true
schemer.valid?(ng_data)  # => false

として、エラー文言を受けとるなら、

errors = schemer.validate(ng_data)  # => #<Enumerator: #<Enumerator::Generator:0x0000000108475348>:each>
errors.to_a                         # => [{"data"=>-1, "data_pointer"=>"/age", "schema"=>{"type"=>"integer", "minimum"=>0}, "schema_pointer"=>"/properties/age", "root_schema"=>{"type"=>"object", "properties"=>{"name"=>{"type"=>"string"}, "age"=>{"type"=>"integer", "minimum"=>0}}, "required"=>["name", "age"]}, "type"=>"minimum", "error"=>"number at `/age` is less than: 0"}]
errors.collect { |e| e["error"] }   # => ["number at `/age` is less than: 0"]

とする。

番外編

dry-schema gem

require "dry-schema"

これは dry の一部で使われているデータ検証ライブラリで少し方向性が異なる。

schema = Dry::Schema.Params do
  required(:name).filled(:string)
  required(:age).filled(:integer, "gteq?" => 0)
end

合っている場合↓

result = schema.call(ok_data)  # => #<Dry::Schema::Result{:name=>"alice", :age=>20} errors={} path=[]>
result.success?                # => true

間違っている場合↓

result = schema.call(ng_data)  # => #<Dry::Schema::Result{:name=>"alice", :age=>-1} errors={:age=>["must be greater than or equal to 0"]} path=[]>
result.success?                # => false
result.errors                  # => #<Dry::Schema::MessageSet messages=[#<Dry::Schema::Message text="must be greater than or equal to 0" path=[:age] predicate=:gteq? input=-1>] options={:failures=>true}>
result.errors.to_h             # => {:age=>["must be greater than or equal to 0"]}

jtd gem

require "jtd"

required を書くと怒られたのでどうやら JSON Schema とは異なるようだ。

schema = {
  "properties" => {
    "name" => { "type" => "string" },
    "age"  => { "type" => "uint32" },
  },
}

いったん JTD::Schema 型にする。

schema = JTD::Schema.from_hash(schema)  # => #<JTD::Schema:0x00000001083d9c18 @ref=nil, @type=nil, @enum=nil, @properties={"name"=>#<JTD::Schema:0x00000001083d98d0 @ref=nil, @type="string", @enum=nil, @discriminator=nil>, "age"=>#<JTD::Schema:0x000000010837fc90 @ref=nil, @type="uint32", @enum=nil, @discriminator=nil>}, @discriminator=nil>

合っている場合↓

JTD::validate(schema, ok_data)  # => []

間違っている場合↓

JTD::validate(schema, ng_data)  # => [#<struct JTD::ValidationError instance_path=["age"], schema_path=["properties", "age", "type"]>]

json-schema と json_schemer の速度比較

json_schemer は速いのが特徴らしいがどのくらい速いのだろうか?

require "active_support/core_ext/benchmark"
def _ = "%.1f ms" % Benchmark.ms { 10000.times { yield } }
_ { JSON::Validator.validate(json_schema, ok_data)  }  # => "492.4 ms"
_ { JSONSchemer.schema(json_schema).valid?(ok_data) }  # => "205.5 ms"

簡単に比較してみると 2.4 倍ほど速かった。

関連

https://techlife.cookpad.com/entry/mart-json-schema
https://github.com/jsontypedef/json-typedef-ruby?tab=readme-ov-file
https://zenn.dev/ningensei848/articles/jtd-in-5-minutes
https://jsontypedef.com/
https://tech-blog.optim.co.jp/entry/2020/12/10/100000

Discussion