
rswag gemを利用してRailsでAPIドキュメントを生成する

IT技術
はじめに
今回は、RailsアプリケーションにSwaggerを用いてAPIドキュメントを導入する方法について解説します。
私自身はGraphQLで開発する機会が多く、これまでSwaggerを使うことがほとんどありませんでした。
そのため、今回初めてSwaggerを使ってみた経験を備忘録として残しておこうと思い、本記事を執筆することにしました。
rswagのインストール
今回は「rswag」というgemを使用します。
こちらはrspecで書いたテストファイルがそのままAPIドキュメントのスキーマになるという優れもので、スキーマと実装の乖離をなくすことができます。
以下をgemに追加します。
1gem 'rswag-api'
2gem 'rswag-ui'
3
4group :development, :test do
5 gem 'rspec-rails'
6 gem 'rswag-specs'
7end
初期設定
簡単のアプリで検証するためscaffoldを使用してユーザーのCRUD操作機能を追加します。
1rails generate scaffold User name:string email:string --api
routes.rbに以下追記
1resources :users
scaffoldで自動生成されたコントローラーは以下のようなコードになります。
1class UsersController < ApplicationController
2 before_action :set_user, only: %i[ show update destroy ]
3
4 # GET /users
5 def index
6 @users = User.all
7
8 render json: @users
9 end
10
11 # GET /users/1
12 def show
13 render json: @user
14 end
15
16 # POST /users
17 def create
18 @user = User.new(user_params)
19
20 if @user.save
21 render json: @user, status: :created, location: @user
22 else
23 render json: @user.errors, status: :unprocessable_entity
24 end
25 end
26
27 # PATCH/PUT /users/1
28 def update
29 if @user.update(user_params)
30 render json: @user
31 else
32 render json: @user.errors, status: :unprocessable_entity
33 end
34 end
35
36 # DELETE /users/1
37 def destroy
38 @user.destroy!
39 end
40
41 private
42 # Use callbacks to share common setup or constraints between actions.
43 def set_user
44 @user = User.find(params.expect(:id))
45 end
46
47 # Only allow a list of trusted parameters through.
48 def user_params
49 params.expect(user: [ :name, :email ])
50 end
51end
マイグレーションを実行してDBにusersテーブルを作成します
1rails db:migrate
rspec上でドキュメント生成をするため、以下のgenerateコマンドを実行していきます。
1rails g rspec:install
2rails g rswag:api:install
3rails g rswag:ui:install
4RAILS_ENV=test rails g rswag:specs:install
次にUsersControllerのスキーマ定義付specファイルを生成するため以下を実行します。
1rails generate rspec:swagger UsersController
実行すると以下のようにスキーマの枠組みが自動生成されます。
便利ですね。
1require 'swagger_helper'
2
3RSpec.describe 'users', type: :request do
4
5 path '/users' do
6
7 get('list users') do
8 response(200, 'successful') do
9
10 after do |example|
11 example.metadata[:response][:content] = {
12 'application/json' => {
13 example: JSON.parse(response.body, symbolize_names: true)
14 }
15 }
16 end
17 run_test!
18 end
19 end
20
21 post('create user') do
22 response(200, 'successful') do
23
24 after do |example|
25 example.metadata[:response][:content] = {
26 'application/json' => {
27 example: JSON.parse(response.body, symbolize_names: true)
28 }
29 }
30 end
31 run_test!
32 end
33 end
34 end
35
36 path '/users/{id}' do
37 # You'll want to customize the parameter types...
38 parameter name: 'id', in: :path, type: :string, description: 'id'
39
40 get('show user') do
41 response(200, 'successful') do
42 let(:id) { '123' }
43
44 after do |example|
45 example.metadata[:response][:content] = {
46 'application/json' => {
47 example: JSON.parse(response.body, symbolize_names: true)
48 }
49 }
50 end
51 run_test!
52 end
53 end
54
55 patch('update user') do
56 response(200, 'successful') do
57 let(:id) { '123' }
58
59 after do |example|
60 example.metadata[:response][:content] = {
61 'application/json' => {
62 example: JSON.parse(response.body, symbolize_names: true)
63 }
64 }
65 end
66 run_test!
67 end
68 end
69
70 put('update user') do
71 response(200, 'successful') do
72 let(:id) { '123' }
73
74 after do |example|
75 example.metadata[:response][:content] = {
76 'application/json' => {
77 example: JSON.parse(response.body, symbolize_names: true)
78 }
79 }
80 end
81 run_test!
82 end
83 end
84
85 delete('delete user') do
86 response(200, 'successful') do
87 let(:id) { '123' }
88
89 after do |example|
90 example.metadata[:response][:content] = {
91 'application/json' => {
92 example: JSON.parse(response.body, symbolize_names: true)
93 }
94 }
95 end
96 run_test!
97 end
98 end
99 end
100end
ドキュメントの生成と確認
仮のスキーマですが、現状ドキュメントの表示の確認してみます。
以下を実行してAPIドキュメントを生成します。
1rake rswag:specs:swaggerize
以下のURLにアクセスしてみます。
http://localhost:3000/api-docs
するとドキュメントが開けました!
ちゃんとrspecに記載したAPIのドキュメントが反映されています。
ただ自動生成されたデータでは不足している定義が多いので編集していきます。
エンドポイントの変更
デフォルトでは「https://www.example.com」がアクセスURLになっているため、ローカル用に変更します。
「swagger_helper.rb」を開き以下のようにURLを変更します
これで指定したエンドポイントでAPIを実行することができます。
1servers: [
2 {
3 url: 'https://{defaultHost}',
4 variables: {
5 defaultHost: {
6 default: 'www.example.com'
7 }
8 }
9 }
10]
11
12↓以下に変更
13
14servers: [
15 {
16 url: 'http://localhost:3000',
17 }
18]
スキーマの更新
現状だと、レスポンスとリクエストパラメーターが定義されていないため追記する必要があります。
その他テストが成功するように実際の挙動に合わせて微修正します。
以下のようにテストファイルを更新しました。
1require 'swagger_helper'
2
3RSpec.describe 'users', type: :request do
4 path '/users' do
5 get('list users') do
6 tags 'Users'
7 produces 'application/json'
8
9 response(200, 'successful') do
10 let!(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
11 schema type: :array,
12 items: {
13 type: :object,
14 properties: {
15 id: { type: :integer },
16 name: { type: :string },
17 email: { type: :string },
18 created_at: { type: :string, format: 'date-time' },
19 updated_at: { type: :string, format: 'date-time' }
20 }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
21 }
22
23 after do |example|
24 example.metadata[:response][:content] = {
25 'application/json' => {
26 example: JSON.parse(response.body, symbolize_names: true)
27 }
28 }
29 end
30 run_test!
31 end
32 end
33
34 post('create user') do
35 tags 'Users'
36 consumes 'application/json'
37
38 parameter name: :user, in: :body, schema: {
39 type: :object,
40 properties: {
41 user: {
42 type: :object,
43 properties: {
44 name: { type: :string, example: 'テスト太郎' },
45 email: { type: :string, example: 'test@example.com' }
46 },
47 required: [ 'name', 'email' ]
48 }
49 },
50 required: [ 'user' ]
51 }
52
53 response(201, 'successful') do
54 let(:user) { { user: { name: 'Test User', email: 'test@example.com' } } }
55
56 after do |example|
57 example.metadata[:response][:content] = {
58 'application/json' => {
59 example: JSON.parse(response.body, symbolize_names: true)
60 }
61 }
62 end
63 run_test!
64 end
65 end
66 end
67
68 path '/users/{id}' do
69 parameter name: 'id', in: :path, type: :string, description: 'User ID'
70
71 get('show user') do
72 tags 'Users'
73 produces 'application/json'
74
75 response(200, 'successful') do
76 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
77 let(:id) { existing_user.id }
78 schema type: :object,
79 properties: {
80 id: { type: :integer },
81 name: { type: :string },
82 email: { type: :string },
83 created_at: { type: :string, format: 'date-time' },
84 updated_at: { type: :string, format: 'date-time' }
85 }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
86
87 after do |example|
88 example.metadata[:response][:content] = {
89 'application/json' => {
90 example: JSON.parse(response.body, symbolize_names: true)
91 }
92 }
93 end
94 run_test!
95 end
96 end
97
98 patch('update user') do
99 tags 'Users'
100 consumes 'application/json'
101 parameter name: :user, in: :body, schema: {
102 type: :object,
103 properties: {
104 user: {
105 type: :object,
106 properties: {
107 name: { type: :string, example: 'テスト太郎' },
108 email: { type: :string, example: 'test@example.com' }
109 }
110 }
111 },
112 required: [ 'user' ]
113 }
114
115 response(200, 'successful') do
116 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
117 let(:id) { existing_user.id }
118 let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
119 schema type: :object,
120 properties: {
121 id: { type: :integer },
122 name: { type: :string },
123 email: { type: :string },
124 created_at: { type: :string, format: 'date-time' },
125 updated_at: { type: :string, format: 'date-time' }
126 }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
127
128 after do |example|
129 example.metadata[:response][:content] = {
130 'application/json' => {
131 example: JSON.parse(response.body, symbolize_names: true)
132 }
133 }
134 end
135 run_test!
136 end
137 end
138
139 put('update user') do
140 tags 'Users'
141 consumes 'application/json'
142 parameter name: :user, in: :body, schema: {
143 type: :object,
144 properties: {
145 user: {
146 type: :object,
147 properties: {
148 name: { type: :string, example: 'テスト太郎' },
149 email: { type: :string, example: 'test@example.com' }
150 }
151 }
152 },
153 required: [ 'user' ]
154 }
155
156 response(200, 'successful') do
157 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
158 let(:id) { existing_user.id }
159 let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
160
161 after do |example|
162 example.metadata[:response][:content] = {
163 'application/json' => {
164 example: JSON.parse(response.body, symbolize_names: true)
165 }
166 }
167 end
168 run_test!
169 end
170 end
171
172 delete('delete user') do
173 tags 'Users'
174
175 response(204, 'no content') do
176 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
177 let(:id) { existing_user.id }
178
179 run_test!
180 end
181 end
182 end
183end
追加したスキーマについて解説します。
レスポンススキーマ
レスポンスの定義方法を以下に示します。
こちらは「オブジェクトの配列である」ことを表しており、プロパティの型や必須設定(required)の定義を行なっています。
1schema type: :array,
2 items: {
3 type: :object,
4 properties: {
5 id: { type: :integer },
6 name: { type: :string },
7 email: { type: :string },
8 created_at: { type: :string, format: 'date-time' },
9 updated_at: { type: :string, format: 'date-time' }
10 }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
11 }
必要があればname: { type: :string, nullable: true}
のようにするとnull設定も変更できます(デフォルトはnotnullです)
リクエストパラメーター
次にリクエストパラメーターの定義について解説します。
パスパラメーターは最初から定義されていましたが、まとめて解説します。
リクエストボディ
リクエストボディはin: :body
とすることで定義できます。
exampleにパラメーター例を指定することで、UI上でパラメーター例を表示することができます。
1parameter name: :user, in: :body, schema: {
2 type: :object,
3 properties: {
4 user: {
5 type: :object,
6 properties: {
7 name: { type: :string, example: 'テスト太郎' },
8 email: { type: :string, example: 'test@example.com' }
9 },
10 required: [ 'name', 'email' ]
11 }
12 },
13 required: [ 'user' ]
14}
パス、クエリストリング
パスはin: :path
、クエリストリングはin: :query
とすることで表現できます。
1parameter name: 'id', in: :path, type: :string, description: 'User ID'
1parameter name: 'id', in: :query, type: :string, description: 'User ID'
スキーマのコンポーネント化
先ほどのスキーマで機能面としては完成しているのですが、同じ構造を毎回再定義しており冗長です。
同構造のレスポンスやリクエストボディはコンポーネントとして共通化することができます。
以下のように「components→schemas」の中にコンポーネントを作成することができ、こちらのコンポーネントを複数のAPIで使い回すことができます。
1components: {
2 schemas: {
3 user_request_body: {
4 type: :object,
5 properties: {
6 user: {
7 type: :object,
8 properties: {
9 name: { type: :string, example: 'テスト太郎' },
10 email: { type: :string, example: 'test@example.com' }
11 },
12 required: [ 'name', 'email' ]
13 }
14 },
15 required: [ 'user' ]
16 },
17 user_response: {
18 type: :object,
19 properties: {
20 id: { type: :integer },
21 name: { type: :string },
22 email: { type: :string },
23 created_at: { type: :string, format: 'date-time' },
24 updated_at: { type: :string, format: 'date-time' }
25 },
26 required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
27 }
28 }
29}
こちらを「spec/swagger_helper.rb」に以下のように定義します。
1config.openapi_specs = {
2 'v1/swagger.yaml' => {
3 openapi: '3.0.1',
4 info: {
5 title: 'API V1',
6 version: 'v1'
7 },
8 paths: {},
9 servers: [
10 {
11 url: 'http://localhost:3000'
12 }
13 ],
14 components: {
15 schemas: {
16 user_request_body: {
17 type: :object,
18 properties: {
19 user: {
20 type: :object,
21 properties: {
22 name: { type: :string, example: 'テスト太郎' },
23 email: { type: :string, example: 'test@example.com' }
24 },
25 required: [ 'name', 'email' ]
26 }
27 },
28 required: [ 'user' ]
29 },
30 user_response: {
31 type: :object,
32 properties: {
33 id: { type: :integer },
34 name: { type: :string },
35 email: { type: :string },
36 created_at: { type: :string, format: 'date-time' },
37 updated_at: { type: :string, format: 'date-time' }
38 },
39 required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
40 }
41 }
42 }
43 }
44}
作成したコンポーネントは'$ref' => '#/components/schemas/コンポーネント名'
とすると参照することができます。
最終的なuser系APIのスキーマは以下のようになります。
1require 'swagger_helper'
2
3RSpec.describe 'users', type: :request do
4 path '/users' do
5 get('list users') do
6 tags 'Users'
7 produces 'application/json'
8
9 response(200, 'successful') do
10 let!(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
11 schema type: :array, items: { '$ref' => '#/components/schemas/user_response' }
12
13 after do |example|
14 example.metadata[:response][:content] = {
15 'application/json' => {
16 example: JSON.parse(response.body, symbolize_names: true)
17 }
18 }
19 end
20 run_test!
21 end
22 end
23
24 post('create user') do
25 tags 'Users'
26 consumes 'application/json'
27 parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
28
29 response(201, 'successful') do
30 let(:user) { { user: { name: 'Test User', email: 'test@example.com' } } }
31
32 after do |example|
33 example.metadata[:response][:content] = {
34 'application/json' => {
35 example: JSON.parse(response.body, symbolize_names: true)
36 }
37 }
38 end
39 run_test!
40 end
41 end
42 end
43
44 path '/users/{id}' do
45 parameter name: 'id', in: :path, type: :string, description: 'User ID'
46
47 get('show user') do
48 tags 'Users'
49 produces 'application/json'
50
51 response(200, 'successful') do
52 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
53 let(:id) { existing_user.id }
54 schema '$ref' => '#/components/schemas/user_response'
55
56 after do |example|
57 example.metadata[:response][:content] = {
58 'application/json' => {
59 example: JSON.parse(response.body, symbolize_names: true)
60 }
61 }
62 end
63 run_test!
64 end
65 end
66
67 patch('update user') do
68 tags 'Users'
69 consumes 'application/json'
70 produces 'application/json'
71 parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
72
73 response(200, 'successful') do
74 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
75 let(:id) { existing_user.id }
76 let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
77 schema '$ref' => '#/components/schemas/user_response'
78
79 after do |example|
80 example.metadata[:response][:content] = {
81 'application/json' => {
82 example: JSON.parse(response.body, symbolize_names: true)
83 }
84 }
85 end
86 run_test!
87 end
88 end
89
90 put('update user') do
91 tags 'Users'
92 consumes 'application/json'
93 produces 'application/json'
94 parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
95
96 response(200, 'successful') do
97 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
98 let(:id) { existing_user.id }
99 let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
100 schema '$ref' => '#/components/schemas/user_response'
101
102 after do |example|
103 example.metadata[:response][:content] = {
104 'application/json' => {
105 example: JSON.parse(response.body, symbolize_names: true)
106 }
107 }
108 end
109 run_test!
110 end
111 end
112
113 delete('delete user') do
114 tags 'Users'
115
116 response(204, 'no content') do
117 let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
118 let(:id) { existing_user.id }
119
120 run_test!
121 end
122 end
123 end
124end
ドキュメント生成(完成系)
ドキュメント生成コマンドを実行します。
1rake rswag:specs:swaggerize RSWAG_DRY_RUN=0
RSWAG_DRY_RUN=0
としているのは、以下のafterブロックがこのオプションを追加しないと反映されないためになります。
afterブロックは、レスポンス例をUIに表示する機能です。
「response.body」をexampleに追加することで、spec内でAPIを実行した結果がUI上のレスポンス例として表示されるようになります。
1after do |example|
2 example.metadata[:response][:content] = {
3 'application/json' => {
4 example: JSON.parse(response.body, symbolize_names: true)
5 }
6 }
7end
再度ドキュメントにアクセスし、表示を確認します。
http://localhost:3000/api-docs
レスポンスやリクエストパラメーターが例付きで定義されていることが確認できます。
作成したコンポーネントもUI上で確認することができます。
最後にrspecを実行してパスすればOKです。
スキーマと実際の挙動が同期していることが確認できました。
1$ rspec
2......
3
4Finished in 0.10116 seconds (files took 0.87154 seconds to load)
56 examples, 0 failures
最後に
今回Swaggerを使用したAPIドキュメントの生成にトライしてみましたが、思った以上に大変でした。
OpenAPI仕様に関して、まだまだ覚えることがたくさんあり学習コストはそれなりにありそうですが、導入することで 以下のようなメリットがあるかと思います。
- APIの仕様を視覚的に確認できる
- ドキュメントの自動生成
- フロントエンドで型に沿った開発ができる
今後、REST APIの開発案件にアサインされたら積極的に導入していこうかと思います。
まだ使用されていない方は、ぜひ導入してみてはいかがでしょうか?
ご愛読ありがとうございました。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ

服部晋平と申します! 前職では映像業や配送業に携わっていました。 趣味は、バイクツーリングに行ったり、美味しいラーメン屋巡りです。 未経験という身で入社させていただいたので、人一倍努力して頑張っていきたいと思います!