Slide 1

Slide 1 text

Generating OpenAPI schema from serializers throughout the Rails stack Andrey Novikov, Evil Martians Kyobashi.rb #5, 2025-02-26 シリアライザーから Railsスタック全体を参考して OpenAPIスキーマの生成方法

Slide 2

Slide 2 text

About me Andrey Novikov ノヴィコフ・アンドレイ Back-end engineer at Evil Martians イービルマーシャンズのバックエンドエンジニア Ruby, Go, PostgreSQL, Docker, k8s… Living in Suita, Osaka for 2 years 2年間以上、大阪府吹田市に住んでいます Love to ride mopeds, motorcycles, and cars over Japan 原付も、大型バイクも、車で日本を旅するのが好き 自己紹介

Slide 3

Slide 3 text

If you have an API you need the schema! For SPA frontends, mobile apps, etc. SPAフロントエンド、モバイルアプリなど For documentation, validation, and code generation ドキュメント、リクエストとレスポンスの検証、コード生成のため Even if you don’t have external clients 外部クライアントがいなくても APIがある場合はスキーマが必要です!

Slide 4

Slide 4 text

Different approaches / 異なるアプローチ Schema First / スキーマファースト Write OpenAPI spec first 最初に OpenAPIスキーマを書く Ensure that implementation matches the spec 実装がスキーマに一致していることを確認 More organized, requires more upfront planning より整理されているが、事前の計画が必要 Recommended in our blog post 当社のブログ記事で推奨 Blog post

Slide 5

Slide 5 text

Different approaches / 異なるアプローチ Implementation First / 実装ファースト Ensures that spec matches the implementation スキーマが実装に一致していることを確認 Simpler for small teams 小規模チームに適している Faster iteration より速い反復開発 Today’s topic 今日の話題

Slide 6

Slide 6 text

Typical Rails API stack Database: stores data and has types データを保存し、データ型の情報を持つ Models: defines relationships and introspects database モデルは関係を定義し、データベースを調査 Controllers: handle requests and responses コントローラーはリクエストとレスポンスを処理 Serializers: generates JSON for API responses from models シリアライザーはモデルから APIレスポンスの JSONを生成 典型的な Rails APIスタック

Slide 7

Slide 7 text

Existing Tools Review Swagger Blocks ❌ Abandoned 開発が停止 ⚠️ Limited OpenAPI 3.0 support OpenAPI 3.0の限定的なサポート Apipie ❌ OpenAPI 2.0 only OpenAPI 2.0のみ ❌ Abandoned 開発が停止 RSwag ✅ Actively maintained アクティブに保守されている ✅ OpenAPI 3 support OpenAPI 3をサポート 既存ツールのレビュー

Slide 8

Slide 8 text

RSwag: Pros and Cons Pros / 長所 ✅ Works well 正常に動作 ✅ Maintained メンテナンスされている ✅ Good integration 良好な統合 Cons / 短所 ❌ No separate type definitions 個別の型定義がない ❌ Manual template maintenance テンプレートの手動管理が必要 ❌ $ref support limitations $refサポートの制限 RSwag:長所と短所

Slide 9

Slide 9 text

Typical RSwag test 典型的な RSwagテスト schema type: :object, properties: { id: { type: :integer }, full_name: { type: :string }, bar: { type: :object, properties: { … }} }, required: [ 'id', 'full_name' ] # spec/requests/api/v1/foos_spec.rb RSpec.describe "/api/v1/foos", openapi_spec: "v1/schema.yml" do path "/v1/foos/{id}" do get "Get a Foo" do parameter name: :page, in: :query, schema: { type: :integer, default: 1 }, require response "200", "A successful response" do run_test! end end

Slide 10

Slide 10 text

❌ No separate type definitions What if we could get type info from serializers… シリアライザーからデータ型情報を取得できたらいいな … 個別の型定義がない class FooSerializer < ActiveModel::Serializer attribute :id # Type can be inferred from the model Foo attribute :full_name do # Need to declare somehow first_name + ' ' + last_name end has_one :bar, serializer: BarSerializer end

Slide 11

Slide 11 text

Introducing Typelizer gem Generates TypeScript type definitions TypeScriptの型定義を生成 Works with several serializer libraries いくつかのシリアライザーライブラリと連携 ActiveModelSerializer Alba Made by a martian Svyatoslav @skryukov 火星人のスヴャトスラフさんが作った gem But how it can help us with OpenAPI schema? しかし、 OpenAPIスキーマにどのように役立つのか? Typelizer gemの紹介 Typelizer gem

Slide 12

Slide 12 text

Let’s hack around and find out! Can we extract and re-use type information without generating TypeScript defs? TypeScriptの定義を生成せずに、型情報を抽出して再利用する方法できるのか? ハックをやってみて、どうなってしまうか見てみよう!

Slide 13

Slide 13 text

Step 1: Add annotations to serializers スキーマにアノテーションを追加 class FooSerializer < ActiveModel::Serializer include Typelizer::DSL attribute :id # Will be inferred from the model Foo typelize :string attribute :full_name do first_name + ' ' + last_name end has_one :bar, serializer: BarSerializer end

Slide 14

Slide 14 text

Step 2: Define RSwag schema template RSwag用のスキーマテンプレートを定義 # spec/swagger_helper.rb RSpec.configure do |config| config.openapi_specs = { "schema.yml" => { openapi: "3.1.0", paths: {}, # RSwag will fill this in components: { schemas: { Typelizer::Generator.new.interfaces.to_h do |interface| [ interface.name, # Magic is here ] end } } } } end

Slide 15

Slide 15 text

Step 3: Convert typelizer data to OpenAPI Typelizerのデータを OpenAPIスキーマに変換 { type: :object, properties: interface.properties.to_h do |property| definition = case property.type when Typelizer::Interface { :$ref => "#/components/schemas/#{property.type.name}" } else { type: property.type.to_s } end definition[:nullable] = true if property.nullable definition[:description] = property.comment if property.comment definition[:enum] = property.enum if property.enum definition = { type: :array, items: definition } if property.multi [ property.name, definition ] end, required: interface.properties.reject(&:optional).map(&:name) }

Slide 16

Slide 16 text

Step 4: Write RSwag specs as usual 通常通り RSwagスペックを記述 schema type: :array, items: { "$ref" => "#/components/schemas/Foo" } # spec/requests/api/v1/foos_spec.rb require "swagger_helper" RSpec.describe "/api/v1/foos", openapi_spec: "v1/schema.yml" do path "/v1/foos" do get "List Foos" do produces "application/json" description "Returns a collection of foos" parameter name: :page, in: :query, schema: { type: :integer, default: 1 }, require response "200", "A successful response" do run_test! end end

Slide 17

Slide 17 text

Hint: AI can rewrite specs to RSwag If there are already controller or request RSpec tests すでにコントローラーまたはリクエスト RSpecテストがある場合 Claude AI can rewrite them to RSwag specs Claude AIはそれらを RSwagスペックに書き換えます Though amount of manual work for re-checking is qute high ただし、再確認のための手作業がかなり多い ヒント: AIはスペックを RSwagにうまく書き換えます

Slide 18

Slide 18 text

And voila! そして、できあがり! $ bundle exec rails rswag paths: /v1/foos: get: responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Foo' components: schemas: Foo: type: object properties: id: type: integer full_name: type: string bar: $ref: '#/components/schemas/Bar'

Slide 19

Slide 19 text

The recipe is Generate schema definitions from serializers シリアライザーから TypeScriptの型を生成 Describe endpoint schemas as RSwag specs エンドポイントスキーマを RSwagスペックとして記述 Reference schema definitions using $ref in specs スペックで $ref を使用してスキーマ定義を参照 レシピは

Slide 20

Slide 20 text

Validating generated schema Use Spectral to validate OpenAPI schema OpenAPIスキーマを検証するために Spectralを使用 Add following check to your Github Actions Github Actionsに以下のチェックを追加 - uses: stoplightio/spectral-action@latest with: file_glob: 'openapi/**/schema.yaml' spectral_ruleset: 'openapi/.spectral.yml' 生成されたスキーマの検証 Spectral

Slide 21

Slide 21 text

Ensuring schema is re-generated Add following check to your Github Actions using plain git commands 次のチェックを、プレーンな gitコマンドを使用して Github Actionsに追加 - name: Re-generate OpenAPI spec and check if it is up-to-date run: | bundle exec rails rswag if [[ -n $(git status --porcelain openapi/) ]]; then echo "::error::OpenAPI documentation is out of date. Please run `rails rswag` locally and comm git status git diff exit 1 fi スキーマが再生成されることを確認

Slide 22

Slide 22 text

Detect breaking changes Use oasdiff to get a diff between two OpenAPI schemas oasdiffを使用して 2つの OpenAPIスキーマの差分を取得 docker run --rm -t -v $(pwd):/specs:ro tufin/oasdiff changelog \ old_schema.yml new_schema.yml -f html > oas_diff.html oasdiff

Slide 23

Slide 23 text

Is it used in production? Yes, at Whop.com! Whop is rapidly growing social commerce platform, development speed is their top priority, and ability to generate OpenAPI schema from serializers is a major pain relief. production環境で使われているのか?

Slide 24

Slide 24 text

Thanks @envek, this setup is pretty sweet, much better than manually editing a massive text file. — Diego Figueroa, staff engineer at Whop.com

Slide 25

Slide 25 text

Thank you! @Envek @Envek @[email protected] @envek.bsky.social github.com/Envek @evilmartians @evilmartians @[email protected] @evilmartians.com evilmartians.com Our awesome blog: evilmartians.com/chronicles! ご清聴ありがとうございました!