https://github.com/exoego/rspec-openapi
Generate OpenAPI schema from RSpec request specs
https://github.com/exoego/rspec-openapi
openapi rails rspec ruby
Last synced: 1 day ago
JSON representation
Generate OpenAPI schema from RSpec request specs
- Host: GitHub
- URL: https://github.com/exoego/rspec-openapi
- Owner: exoego
- License: mit
- Created: 2020-06-16T06:27:33.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2026-02-24T02:51:47.000Z (8 days ago)
- Last Synced: 2026-02-24T08:56:45.073Z (8 days ago)
- Topics: openapi, rails, rspec, ruby
- Language: Ruby
- Homepage:
- Size: 1000 KB
- Stars: 495
- Watchers: 8
- Forks: 69
- Open Issues: 15
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# rspec-openapi [](https://rubygems.org/gems/rspec-openapi) [](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [](https://codecov.io/gh/exoego/rspec-openapi) [](https://www.ruby-toolbox.com/projects/rspec-openapi) [](https://deepwiki.com/exoego/rspec-openapi)
Generate OpenAPI schema from RSpec request specs.
## What's this?
There are some gems which generate OpenAPI specs from RSpec request specs.
However, they require a special DSL specific to these gems, and we can't reuse existing request specs as they are.
Unlike such [existing gems](#links), rspec-openapi can generate OpenAPI specs from request specs without requiring any special DSL.
Furthermore, rspec-openapi keeps manual modifications when it merges automated changes to OpenAPI specs
in case we can't generate everything from request specs.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'rspec-openapi', group: :test
```
## Usage
Run rspec with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
```bash
$ OPENAPI=1 bundle exec rspec
```
### Example
Let's say you have [a request spec](https://github.com/exoego/rspec-openapi/blob/24e5c567c2e90945c7a41f19f71634ac028cc314/spec/requests/rails_spec.rb#L38) like this:
```rb
RSpec.describe 'Tables', type: :request do
describe '#index' do
it 'returns a list of tables' do
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
expect(response.status).to eq(200)
end
it 'does not return tables if unauthorized' do
get '/tables'
expect(response.status).to eq(401)
end
end
# ...
end
```
If you run the spec with `OPENAPI=1`,
```
OPENAPI=1 bundle exec rspec spec/requests/tables_spec.rb
```
It will generate [`doc/openapi.yaml` file](./spec/rails/doc/openapi.yaml) like:
```yml
openapi: 3.0.3
info:
title: rspec-openapi
paths:
"/tables":
get:
summary: index
tags:
- Table
parameters:
- name: page
in: query
schema:
type: integer
example: 1
- name: per
in: query
schema:
type: integer
example: 10
responses:
'200':
description: returns a list of tables
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
# ...
```
and the schema file can be used as an input of [Swagger UI](https://github.com/swagger-api/swagger-ui) or [Redoc](https://github.com/Redocly/redoc).

### Configuration
The following configurations are optional.
```rb
require 'rspec/openapi'
# Change the path to generate schema from `doc/openapi.yaml`
RSpec::OpenAPI.path = 'doc/schema.yaml'
# Change the output type to JSON
RSpec::OpenAPI.path = 'doc/schema.json'
# Or generate multiple partial schema files, given an RSpec example
RSpec::OpenAPI.path = -> (example) {
case example.file_path
when %r[spec/requests/api/v1/] then 'doc/openapi/v1.yaml'
when %r[spec/requests/api/v2/] then 'doc/openapi/v2.yaml'
else 'doc/openapi.yaml'
end
}
# Change the default title of the generated schema
RSpec::OpenAPI.title = 'OpenAPI Documentation'
# Or generate individual titles for your partial schema files, given an RSpec example
RSpec::OpenAPI.title = -> (example) {
case example.file_path
when %r[spec/requests/api/v1/] then 'API v1 Documentation'
when %r[spec/requests/api/v2/] then 'API v2 Documentation'
else 'OpenAPI Documentation'
end
}
# Disable generating `example` globally
RSpec::OpenAPI.enable_example = false
# Customize example name generation (used for multiple examples)
RSpec::OpenAPI.example_name_builder = -> (example) { example.description }
# Disable generating example summaries for `examples`
RSpec::OpenAPI.enable_example_summary = false
# Change `info.version`
RSpec::OpenAPI.application_version = '1.0.0'
# Set the info header details
RSpec::OpenAPI.info = {
description: 'My beautiful API',
license: {
'name': 'Apache 2.0',
'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'
}
}
# Set request `headers` - generate parameters with headers for a request
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
# Set response `headers` - generate parameters with headers for a response
RSpec::OpenAPI.response_headers = %w[X-Cursor]
# Set `servers` - generate servers of a schema file
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
# Set `security_schemes` - generate security schemes
RSpec::OpenAPI.security_schemes = {
'MyToken' => {
description: 'Authenticate API requests via a JWT',
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
}
# Generate a comment on top of a schema file
RSpec::OpenAPI.comment = <<~EOS
This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
update this file automatically. You can also manually edit this file.
EOS
# Generate a custom description, given an RSpec example
RSpec::OpenAPI.description_builder = -> (example) { example.description }
# Generate a custom summary, given an RSpec example
# This example uses the summary from the example_group.
RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_group, :openapi, :summary) }
# Generate a custom tags, given an RSpec example
# This example uses the tags from the parent_example_group
RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
# Configure custom format for specific properties
# This example assigns 'date-time' format to properties with names ending in '_at'
RSpec::OpenAPI.formats_builder = ->(_example, key) { key.end_with?('_at') ? 'date-time' : nil }
# Change the example type(s) that will generate schema
RSpec::OpenAPI.example_types = %i[request]
# Configure which path params to ignore
# :controller and :action always exist. :format is added when routes is configured as such.
RSpec::OpenAPI.ignored_path_params = %i[controller action format]
# Configure which paths to ignore.
# You can exclude some specs via `openapi: false`.
# But, in a complex API usage scenario, you may need to include spec itself, but exclude some private paths.
# In that case, you can specify the paths to ignore.
# String or Regexp is acceptable.
RSpec::OpenAPI.ignored_paths = ["/admin/full/path/", Regexp.new("^/_internal/")]
# Your custom post-processing hook (like unrandomizing IDs)
RSpec::OpenAPI.post_process_hook = -> (path, records, spec) do
RSpec::OpenAPI::HashHelper.matched_paths(spec, 'paths.*.*.responses.*.content.*.*.*.id').each do |paths|
spec.dig(*paths[0..-2]).merge!(id: '123')
end
end
```
### Can I use rspec-openapi with `$ref` to minimize duplication of schema?
Yes, rspec-openapi v0.7.0+ supports [`$ref` mechanism](https://swagger.io/docs/specification/using-ref/) and generates
schemas under `#/components/schemas` with some manual steps.
1. First, generate plain OpenAPI file.
2. Then, manually replace the duplications with `$ref`.
```yaml
paths:
"/users":
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
"/users/{id}":
get:
responses:
'200':
content:
application/json:
schema:
$ref: "#/components/schemas/User"
# Note) #/components/schemas is not needed to be defined.
```
3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.
```yaml
paths:
"/users":
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
"/users/{id}":
get:
responses:
'200':
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
role:
type: array
items:
type: string
```
rspec-openapi also supports `$ref` in `properties` of schemas. Example)
```yaml
paths:
"/locations":
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Location"
components:
schemas:
Location:
type: object
properties:
id:
type: string
name:
type: string
Coordinate:
"$ref": "#/components/schemas/Coordinate"
Coordinate:
type: object
properties:
lat:
type: string
lon:
type: string
```
Note that automatic `schemas` update feature is still new and may not work in complex scenario.
If you find a room for improvement, open an issue.
### How can I add information which can't be generated from RSpec?
rspec-openapi tries to preserve manual modifications as much as possible when generating specs.
You can directly edit `doc/openapi.yaml` as you like without spoiling the automatic generation capability.
### Can I exclude specific specs from OpenAPI generation?
Yes, you can specify `openapi: false` to disable the automatic generation.
```rb
RSpec.describe '/resources', type: :request, openapi: false do
# ...
end
# or
RSpec.describe '/resources', type: :request do
it 'returns a resource', openapi: false do
# ...
end
end
```
## Customizations
Some examples' attributes can be overwritten via RSpec metadata options. Example:
```rb
describe 'GET /api/v1/posts', openapi: {
summary: 'list all posts',
description: 'list all posts ordered by pub_date',
tags: %w[v1 posts],
required_request_params: %w[limit],
security: [{"MyToken" => []}],
} do
# ...
end
```
**NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
### Enum Support
You can specify enum values for string properties that should have a fixed set of allowed values. Since enums cannot be reliably inferred from test data, you can define them via the `enum` metadata option:
```rb
it 'returns user status', openapi: {
enum: {
'status' => %w[active inactive suspended],
},
} do
get '/users/1'
expect(response.status).to eq(200)
end
```
This generates:
```yaml
schema:
type: object
properties:
status:
type: string
enum:
- active
- inactive
- suspended
```
#### Nested Paths
For nested objects, use dot notation to specify the path:
```rb
it 'returns user with role', openapi: {
enum: {
'status' => %w[active inactive],
'user.role' => %w[admin user guest],
},
} do
get '/teams/1'
# Response: { "status": "active", "user": { "name": "John", "role": "admin" } }
expect(response.status).to eq(200)
end
```
#### Array Items
For properties inside array items, use the array property name followed by the item property:
```rb
it 'returns items with status', openapi: {
enum: {
'items.status' => %w[pending completed failed],
'items.priority' => %w[high medium low],
},
} do
get '/tasks'
# Response: { "items": [{ "id": 1, "status": "pending", "priority": "high" }] }
expect(response.status).to eq(200)
end
```
#### Request vs Response Enums
By default, `enum` applies to both request and response bodies. If you need different enum values for request and response, use `request_enum` and `response_enum`:
```rb
it 'creates a task', openapi: {
request_enum: {
'action' => %w[create update delete],
},
response_enum: {
'status' => %w[pending processing completed],
},
} do
post '/tasks', params: { action: 'create', name: 'New Task' }
expect(response.status).to eq(201)
end
```
### Multiple Examples Mode
You can generate multiple named examples for the same endpoint using `example_mode`:
```rb
describe '#index', openapi: { example_mode: :multiple } do
it 'with pagination' do
get '/tables', params: { page: 1, per: 10 }
expect(response.status).to eq(200)
end
it 'with filter' do
get '/tables', params: { filter: { name: 'test' } }
expect(response.status).to eq(200)
end
end
```
This generates OpenAPI with multiple named examples:
```yaml
responses:
'200':
content:
application/json:
schema: { ... }
examples:
with_pagination:
value: { ... }
with_filter:
value: { ... }
```
Available `example_mode` values:
- `:single` (default) - generates single `example` field
- `:multiple` - generates named `examples` with test descriptions as keys
- `:none` - generates only schema, no examples
The mode is inherited by nested contexts and can be overridden at any level.
**Note:** If multiple examples resolve to the same example key for a single endpoint, the last one wins (overwrites).
#### Merge Behavior with Mixed Modes
When multiple tests target the same endpoint with different `example_mode` settings (even from different spec files), the merger automatically converts to `examples` format:
```rb
# spec/requests/api_spec.rb
describe 'GET /users' do
it 'returns users' do # default :single mode
get '/users'
expect(response.status).to eq(200)
end
end
# spec/requests/admin_spec.rb
describe 'GET /users', openapi: { example_mode: :multiple } do
it 'with admin privileges' do
get '/users', headers: { 'X-Admin': 'true' }
expect(response.status).to eq(200)
end
end
```
Result - both examples merged into `examples`:
```yaml
responses:
'200':
content:
application/json:
examples:
returns_users:
value: { ... }
with_admin_privileges:
value: { ... }
```
To exclude specific tests from example generation, use `example_mode: :none`:
```rb
describe 'GET /users', openapi: { example_mode: :none } do
it 'edge case test' do
# This won't add examples to OpenAPI spec
end
end
```
## Experimental minitest support
Even if you are not using `rspec` this gem might help you with its experimental support for `minitest`.
Example:
```rb
class TablesTest < ActionDispatch::IntegrationTest
openapi!
test "GET /index returns a list of tables" do
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
assert_response :success
end
test "GET /index does not return tables if unauthorized" do
get '/tables'
assert_response :unauthorized
end
# ...
end
```
It should work with both classes inheriting from `ActionDispatch::IntegrationTest` and with classes using `Rack::Test` directly, as long as you call `openapi!` in your test class.
Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per test case metadata is not supported. A custom `description_builder` will not work either.
Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
```bash
$ OPENAPI=1 bundle exec rails t
```
## Links
Existing RSpec plugins which have OpenAPI integration:
* [zipmark/rspec\_api\_documentation](https://github.com/zipmark/rspec_api_documentation)
* [rswag/rswag](https://github.com/rswag/rswag)
* [drewish/rspec-rails-swagger](https://github.com/drewish/rspec-rails-swagger)
## Acknowledgements
* Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
* Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.
## Releasing
1. Ensure RubyGems trusted publishing is configured for this repo and gem ownership (see [Trusted publishing](https://guides.rubygems.org/trusted-publishing/)).
2. In GitHub Actions, run the `prepare release` workflow manually. It bumps `lib/rspec/openapi/version.rb`, pushes `release/v` to origin, and opens a PR.
3. Review and merge the release PR into the default branch.
4. Create and push a tag `v` on the merged commit (via the GitHub UI or `git tag v; git push origin v`). Tag creation triggers the `Publish to RubyGems` workflow, which publishes the gem and creates the GitHub release notes automatically.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).