🛠

OpenAPI のレスポンス定義では additionalProperties: false したほうが良い

Leaner 開発チームの黒曜(@kokuyouwind)です。

Leaner の技術スタック紹介(2021年7月版) で、新規プロダクトでは OpenAPI を用いスキーマ検証する話に触れました。
今回はこれに関するちょっとした Tips です。

TL;DR

Committee::Rails を使うときはレスポンス定義で additionalProperties: false を指定しておくと、定義以外の属性でテストが引っかかるので安全でした。

Committee:Rails を利用したスキーマ検証

rspec-request_describer と Committee:Rails を導入すると、以下のように Request Spec を書くだけでレスポンススキーマの検証が行えます。

# spec/support/match_schema_definition_shared_examples.rb
RSpec.shared_examples 'match schema definition' do |status: 200|
 it "スキーマ定義の status:#{status} に合致したレスポンスを返す" do
   subject
   expect(response.status).to eq(status)
   assert_response_schema_confirm(status)
 end
end

# spec/requests/users_spec.rb
RSpec.describe 'Users', type: :request do
 describe 'GET /user' do
   context 'ログイン済みのとき' do
     include_context 'logged in as a user'

     it_behaves_like 'match schema definition'
   end

   context '未ログインのとき' do
     it_behaves_like 'match schema definition', status: 401
   end
 end
end

この際、単純にスキーマを記述するだけでは、レスポンスに余分な属性が含まれても検証に通ってしまいます。

// schema.json 
// GET /user のレスポンス定義
"properties": {
  "id": {
    "type": "integer",
    "minimum": 1
  },
  "email": {
    "type": "string",
    "format": "email"
  },
  "name": {
    "type": "string"
  },
  "required": [
    "id",
    "email"
  ]
},
class UsersController < ApplicationController
  def show
    render json: user.as_json # created_at, updated_at なども含まれている
  end
end
$ bundle exec rspec spec/requests/users_spec.rb

Finished in 2.45 seconds (files took 0.56858 seconds to load)
# テストが通ってしまう!

created_at などであれば大した問題ではありませんが、例えば他ユーザの本名や IP アドレスが意図せず API レスポンスに含まれてしまうと大問題になりえます。

こうした挙動を防ぐには、レスポンス定義に additionalProperties: false を指定します。

"properties": {
  // ...
},
"additionalProperties": false, // <-- 追加

すると、 spec 実行時に余分な属性値を検出しテストが失敗するようになります。

$ bundle exec rspec spec/requests/users_spec.rb

Failures:
  1) Users POST /user/image ログイン済みのとき behaves like match schema definition スキーマ定義の status:200 に合致したレスポンスを返す
     Failure/Error: assert_response_schema_confirm(status)

     Committee::InvalidResponse:
       #/components/schemas/User does not define properties: createdAt, updatedAt

なお components を使って定義したモデルを参照している場合は、モデル側で additionalProperties を設定する必要があります。

"User": {
  "type": "object",
  "additionalProperties": false,
  "properties": {
    // ...

どうやってこの設定に辿り着いたか

Committee の Issueresponse extraresponse unknown などで検索して、以下の Issue にたどり着きました。[1]

https://github.com/interagent/committee/issues/263

こちらはレスポンスではなくリクエストパラメタについての話でしたが、回答で触れられている additionalProperties: false がレスポンス定義でも使えるか試したところうまく動いた、という流れです。

調べ始めた段階では Committee 側の設定の問題だと思っていたので、 OpenAPI 定義の話にたどり着けたのは幸運でした。それと同時に、やはり大体同じ疑問を持ってる人がいるので Issue を漁るのは大事ですね。

参考文献

Committee:Rails を用いたスキーマ検証については GiFT 様のcommittee×OpenAPI×RailsでスキーマファーストなAPI開発を参考にさせていただいています。

こちらの記事に書かれた以下の記述が気になり、方法がないか調べた結果 additionalProperties の設定を見つけることができました。

また、仕様には記載していない created_at や updated_at がレスポンスに含まれていてもテストは通ってしまいます。

https://gift-tech.co.jp/articles/schema-first-api-development/

他に日本語で additionalProperties について触れた記事がないか探した結果、以下の記事を見つけました。

こちらでは allOf を用いた場合などについても触れられています。

https://sakaishun.com/2021/04/18/committee-schema-check/

宣伝

Leaner Technologies では Committee でスキーマ駆動な API 開発をしたいエンジニアを募集しています!

https://careers.leaner.co.jp/
脚注
  1. additional という単語が思いつかなかったので、それっぽい単語を色々入れて試していました。 ↩︎

リーナーテックブログ

Leaner Technologiesのテックブログです! 採用情報: careers.leaner.co.jp/

Discussion

ログインするとコメントできます