JsonWorldでモデルからJSON Schemaを生成する
JsonWorldというGemを使って、JSON Schemaを生成できるモデルクラスをつくります。Qiita API v2のJSON Schemaを公開しました - Qiita Blog の裏側で使っているやつです。
JsonWorldの使い方
JsonWorld::DSL というModuleが提供されているので、これを任意のClassにincludeします。このmoduleをincludeしたClassでは、以下の特異メソッドが利用できるようになります。propertyとlinkを利用してインターフェースを定義し、to_json_schemaでJSON Schemaとしての表現を取り出す、というのがJsonWorldの使い方です。
.property(property_name, options)
.link(link_name, options)
.to_json_schema
JsonWorld::DSL
「Integer型のidプロパティを持つ」という情報を定義したJSON Schemaを生成することにします。Userクラスのインスタンスを持った、UserResourceクラスを用意して、これにincludeして使うことにします。
require "json_world"
class UserResource
include JsonWorld::DSL
property(
:id,
type: Integer,
)
# @param [User] user
def initialize(user)
@user = user
end
# @return [Integer]
def id
@user.id
end
end
propertyの第一引数にはそのリソースが取り得るプロパティの名前をSymbolで、第二引数にはそのプロパティの性質をHashで指定できます。typeを指定すると、idというプロパティの型がInteger型であるという情報が定義されます。Rubyのクラスオブジェクトを渡していますが、これはJSON Schemaにおいて { "type": "integer" }
という情報に変換されます。.to_json_schema
を呼ぶと、それっぽいやつが取り出せます。
puts UserResource.to_json_schema
{
"properties": {
"id": {
"type": "integer"
}
},
"required": [
"id"
]
}
なお、JsonWorld::DSL は .property
で定義したプロパティを利用して #as_json
の実装を適当に用意してくれます。active_support/json を読み込んでいる環境で、オブジェクトがこのメソッドを実装していると、#to_json
がこの結果をJSONに変換したものになります。よって、あるクラスにJsonWorld::DSLをincludeしていれば、そのクラスのインスタンスは #to_json
に対応できます。
user = User.new
user_resource = UserResource.new(user)
user_resource.to_json #=> '{"id":1}'
.property
あとは .property と .link の細かいオプションの説明です。.property に渡せるオプションを見ていきましょう。
- :description
- :example
- :items
- :optional
- :pattern
- :properties
- :type
- :unique
:description
これは、JSON Schemaにおける description プロパティを定義するものです。このプロパティが何であるかを説明するやつです。Qiitaでは日本語と英語のスキーマを用意する必要があるため、I18n.translate
を利用した値を :description オプションとして渡しています。
property(
:id,
description: "A unique user ID of this user",
)
:example
これは、JSON Schemaにおいて example プロパティを定義するものです。このプロパティは例えばこういう値を取り得るよというやつですね。Qiita API v2のドキュメントは jdoc でJSON Schemaから生成してるんですが、ここではexampleの値を元に自動的にサンプルリクエストとサンプルレスポンスを生成しています。
property(
:id,
example: 1,
)
:items
と :unique
これは、JSON Schemaにおいて items プロパティを定義するものです。配列の要素がどういうものかということを表現するための値なので、typeがArrayのときだけ利用します。:items
の値には .property
の第二引数に渡せるものと同じオプションが渡せます (つまりネストできるということです)。以下の例では、tagsプロパティは文字列型の要素を含む配列であるということを示しています。また、このとき :unique をtrueに指定すると、中の要素は一意であるということを示せます。これは、JSON Schemaにおいては uniqueItems プロパティとして表現されます。
property(
:tags,
items: {
type: String,
},
unique: true,
)
:optional
これは、JSON Schemaにおいてrequiredプロパティに影響する値です。JsonWorldでObject相当のデータを定義するとき、基本的には全てのプロパティは必須のものとして扱いますが、:optionalにtrueが指定されているものは必須にはなりません。例えば、記事一覧を取得するエンドポイントでは、ページネーション用のパラメータは必須ではないので、この機能を利用して定義できます。
link(
:list_items,
parameters: {
page: {
example: 2,
optional: true,
type: Integer,
},
}
)
:pattern
これは、JSON Schemaにおいてpatternプロパティを定義するものです。値が文字列型のときに、更に正規表現を指定して有効な値を限定するためのやつです。
property(
:name,
pattern: /^.{3,20}$/,
type: String,
)
:properties
これは、JSON Schemaにおいてpropertiesプロパティを定義するものです。値がObject型だったときに、更にネストしてそれぞれのプロパティの情報を定義できるというやつですね。
property(
:stats,
properties: {
items_count: {
type: Integer,
},
stocks_count: {
type: Integer,
},
},
)
:type
これは、JSON Schemaにおいてtypeプロパティを定義するものです。文字列を渡すとその値が利用されますが、Classクラスのインスタンス (※ ArrayとかStringとかです) で対応しているものを渡すと、自動的に対応する文字列に変換してくれます。例えば、Stringを指定すると"string"に、Integerを指定すると"integer"になります。
property(
:id,
type: Integer,
)
.link
JSON Hyper-Schemaでは、個々のリソースがどのような操作やURLに関係しているかということを定義できます。JsonWorld::DSLでは、以下のオプションとともに .link
というメソッドを利用してこれを定義できます。
- :description
- :method
- :parameters
- :path
- :rel
- :title
class UserResource
include JsonWorld::DSL
link(
:list_users,
description: "List users in newest order",
path: "/api/users",
parameters: {
page: {
example: 2,
optional: true,
type: Integer,
},
},
)
end
puts UserResource.to_json_schema
{
"links": [
{
"description": "List users in newest order",
"href": "/api/users",
"method": "GET",
"schema": {
"properties": {
"page": {
"example": 2,
"type": "integer"
}
}
},
"title": "list_users"
}
]
}
リソースをまとめる
.property の :type オプションには、JsonWorld::DSLをincludeしたクラスも渡せます。なので、こういう感じで幾つかのリソースをまとめるためのリソースを用意すれば、1つの大きなスキーマを生成できます。
module Api
class Schema
include JsonWorld::DSL
property(:access_token, links: true, type: Api::Resources::AccessToken)
property(:authenticated_user, links: true, type: Api::Resources::AuthenticatedUser)
property(:comment, links: true, type: Api::Resources::Comment)
...
end
end
puts Api::Schema.to_json_schema
おわり
JsonWorldというGemを使って、JSON Schemaを生成できるモデルクラスをつくる方法について説明しました。ここで生成したスキーマを使って、サーバ側の実装に再利用したり、各種言語のクライアントライブラリを用意したりできます。多くの場合はあまり参考にならないとは思いますが、こういう風にやっている人も居るんだなあぐらいの気持ちで見といてもらえると幸いです。