{"id":13879156,"url":"https://github.com/exoego/rspec-openapi","last_synced_at":"2026-03-02T14:01:13.391Z","repository":{"id":37671601,"uuid":"272627449","full_name":"exoego/rspec-openapi","owner":"exoego","description":"Generate OpenAPI schema from RSpec request specs","archived":false,"fork":false,"pushed_at":"2026-02-24T02:51:47.000Z","size":1028,"stargazers_count":495,"open_issues_count":15,"forks_count":69,"subscribers_count":8,"default_branch":"master","last_synced_at":"2026-02-24T08:56:45.073Z","etag":null,"topics":["openapi","rails","rspec","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/exoego.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"k0kubun"}},"created_at":"2020-06-16T06:27:33.000Z","updated_at":"2026-02-24T02:44:41.000Z","dependencies_parsed_at":"2023-09-29T07:50:53.180Z","dependency_job_id":"e4d1f1d7-14b6-4a3b-ab5e-3e32903c605b","html_url":"https://github.com/exoego/rspec-openapi","commit_stats":{"total_commits":413,"total_committers":40,"mean_commits":10.325,"dds":0.6368038740920097,"last_synced_commit":"988f87fb2d85650261279e351a97dac3633f73f5"},"previous_names":["k0kubun/rspec-openapi"],"tags_count":75,"template":false,"template_full_name":null,"purl":"pkg:github/exoego/rspec-openapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoego%2Frspec-openapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoego%2Frspec-openapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoego%2Frspec-openapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoego%2Frspec-openapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/exoego","download_url":"https://codeload.github.com/exoego/rspec-openapi/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoego%2Frspec-openapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30002187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T12:19:43.414Z","status":"ssl_error","status_checked_at":"2026-03-02T12:19:02.215Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["openapi","rails","rspec","ruby"],"created_at":"2024-08-06T08:02:11.552Z","updated_at":"2026-03-02T14:01:13.379Z","avatar_url":"https://github.com/exoego.png","language":"Ruby","readme":"# rspec-openapi [![Gem Version](https://badge.fury.io/rb/rspec-openapi.svg)](https://rubygems.org/gems/rspec-openapi) [![test](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml/badge.svg)](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/exoego/rspec-openapi/branch/master/graph/badge.svg?token=egYm6AlxkD)](https://codecov.io/gh/exoego/rspec-openapi) [![Ruby-toolbox](https://img.shields.io/badge/ruby-toolbox-a61414?cacheSeconds=31536000)](https://www.ruby-toolbox.com/projects/rspec-openapi) [![DeepWiki](https://img.shields.io/badge/See_on-DeepWiki-blue)](https://deepwiki.com/exoego/rspec-openapi)\n\n\nGenerate OpenAPI schema from RSpec request specs.\n\n## What's this?\n\nThere are some gems which generate OpenAPI specs from RSpec request specs.\nHowever, they require a special DSL specific to these gems, and we can't reuse existing request specs as they are.\n\nUnlike such [existing gems](#links), rspec-openapi can generate OpenAPI specs from request specs without requiring any special DSL.\nFurthermore, rspec-openapi keeps manual modifications when it merges automated changes to OpenAPI specs\nin case we can't generate everything from request specs.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'rspec-openapi', group: :test\n```\n\n## Usage\n\nRun rspec with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.\n\n```bash\n$ OPENAPI=1 bundle exec rspec\n```\n\n### Example\n\nLet's say you have [a request spec](https://github.com/exoego/rspec-openapi/blob/24e5c567c2e90945c7a41f19f71634ac028cc314/spec/requests/rails_spec.rb#L38) like this:\n\n```rb\nRSpec.describe 'Tables', type: :request do\n  describe '#index' do\n    it 'returns a list of tables' do\n      get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }\n      expect(response.status).to eq(200)\n    end\n\n    it 'does not return tables if unauthorized' do\n      get '/tables'\n      expect(response.status).to eq(401)\n    end\n  end\n\n  # ...\nend\n```\n\nIf you run the spec with `OPENAPI=1`,\n\n```\nOPENAPI=1 bundle exec rspec spec/requests/tables_spec.rb\n```\n\nIt will generate [`doc/openapi.yaml` file](./spec/rails/doc/openapi.yaml) like:\n\n```yml\nopenapi: 3.0.3\ninfo:\n  title: rspec-openapi\npaths:\n  \"/tables\":\n    get:\n      summary: index\n      tags:\n      - Table\n      parameters:\n      - name: page\n        in: query\n        schema:\n          type: integer\n        example: 1\n      - name: per\n        in: query\n        schema:\n          type: integer\n        example: 10\n      responses:\n        '200':\n          description: returns a list of tables\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    id:\n                      type: integer\n                    name:\n                      type: string\n                    # ...\n```\n\nand 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).\n\n![Redoc example](./spec/apps/rails/doc/screenshot.png)\n\n\n### Configuration\n\nThe following configurations are optional.\n\n```rb\nrequire 'rspec/openapi'\n\n# Change the path to generate schema from `doc/openapi.yaml`\nRSpec::OpenAPI.path = 'doc/schema.yaml'\n\n# Change the output type to JSON\nRSpec::OpenAPI.path = 'doc/schema.json'\n\n# Or generate multiple partial schema files, given an RSpec example\nRSpec::OpenAPI.path = -\u003e (example) {\n  case example.file_path\n  when %r[spec/requests/api/v1/] then 'doc/openapi/v1.yaml'\n  when %r[spec/requests/api/v2/] then 'doc/openapi/v2.yaml'\n  else 'doc/openapi.yaml'\n  end\n}\n\n# Change the default title of the generated schema\nRSpec::OpenAPI.title = 'OpenAPI Documentation'\n\n# Or generate individual titles for your partial schema files, given an RSpec example\nRSpec::OpenAPI.title = -\u003e (example) {\n  case example.file_path\n  when %r[spec/requests/api/v1/] then 'API v1 Documentation'\n  when %r[spec/requests/api/v2/] then 'API v2 Documentation'\n  else 'OpenAPI Documentation'\n  end\n}\n\n# Disable generating `example` globally\nRSpec::OpenAPI.enable_example = false\n\n# Customize example name generation (used for multiple examples)\nRSpec::OpenAPI.example_name_builder = -\u003e (example) { example.description }\n\n# Disable generating example summaries for `examples`\nRSpec::OpenAPI.enable_example_summary = false\n\n# Change `info.version`\nRSpec::OpenAPI.application_version = '1.0.0'\n\n# Set the info header details\nRSpec::OpenAPI.info = {\n  description: 'My beautiful API',\n  license: {\n    'name': 'Apache 2.0',\n    'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'\n  }\n}\n\n# Set request `headers` - generate parameters with headers for a request\nRSpec::OpenAPI.request_headers = %w[X-Authorization-Token]\n\n# Set response `headers` - generate parameters with headers for a response\nRSpec::OpenAPI.response_headers = %w[X-Cursor]\n\n# Set `servers` - generate servers of a schema file\nRSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]\n\n# Set `security_schemes` - generate security schemes\nRSpec::OpenAPI.security_schemes = {\n  'MyToken' =\u003e {\n    description: 'Authenticate API requests via a JWT',\n    type: 'http',\n    scheme: 'bearer',\n    bearerFormat: 'JWT',\n  },\n}\n\n# Generate a comment on top of a schema file\nRSpec::OpenAPI.comment = \u003c\u003c~EOS\n  This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi\n\n  When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will\n  update this file automatically. You can also manually edit this file.\nEOS\n\n# Generate a custom description, given an RSpec example\nRSpec::OpenAPI.description_builder = -\u003e (example) { example.description }\n\n# Generate a custom summary, given an RSpec example\n# This example uses the summary from the example_group.\nRSpec::OpenAPI.summary_builder = -\u003e(example) { example.metadata.dig(:example_group, :openapi, :summary) }\n\n# Generate a custom tags, given an RSpec example\n# This example uses the tags from the parent_example_group\nRSpec::OpenAPI.tags_builder = -\u003e (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }\n\n# Configure custom format for specific properties\n# This example assigns 'date-time' format to properties with names ending in '_at'\nRSpec::OpenAPI.formats_builder = -\u003e(_example, key) { key.end_with?('_at') ? 'date-time' : nil }\n\n# Change the example type(s) that will generate schema\nRSpec::OpenAPI.example_types = %i[request]\n\n# Configure which path params to ignore\n# :controller and :action always exist. :format is added when routes is configured as such.\nRSpec::OpenAPI.ignored_path_params = %i[controller action format]\n\n# Configure which paths to ignore.\n# You can exclude some specs via `openapi: false`.\n# But, in a complex API usage scenario, you may need to include spec itself, but exclude some private paths.\n# In that case, you can specify the paths to ignore.\n# String or Regexp is acceptable.\nRSpec::OpenAPI.ignored_paths = [\"/admin/full/path/\", Regexp.new(\"^/_internal/\")]\n\n# Your custom post-processing hook (like unrandomizing IDs)\nRSpec::OpenAPI.post_process_hook = -\u003e (path, records, spec) do\n  RSpec::OpenAPI::HashHelper.matched_paths(spec, 'paths.*.*.responses.*.content.*.*.*.id').each do |paths|\n    spec.dig(*paths[0..-2]).merge!(id: '123')\n  end\nend\n```\n\n### Can I use rspec-openapi with `$ref` to minimize duplication of schema?\n\nYes, rspec-openapi v0.7.0+ supports [`$ref` mechanism](https://swagger.io/docs/specification/using-ref/) and generates\nschemas under `#/components/schemas` with some manual steps.\n\n1. First, generate plain OpenAPI file.\n2. Then, manually replace the duplications with `$ref`.\n\n```yaml\npaths:\n  \"/users\":\n    get:\n      responses:\n        '200':\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: \"#/components/schemas/User\"\n  \"/users/{id}\":\n    get:\n      responses:\n        '200':\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/User\"\n# Note) #/components/schemas is not needed to be defined.\n```\n\n3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.\n\n```yaml\npaths:\n  \"/users\":\n    get:\n      responses:\n        '200':\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: \"#/components/schemas/User\"\n  \"/users/{id}\":\n    get:\n      responses:\n        '200':\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/User\"\ncomponents:\n  schemas:\n    User:\n      type: object\n      properties:\n        id:\n          type: string\n        name:\n          type: string\n        role:\n          type: array\n          items:\n            type: string\n```\n\nrspec-openapi also supports `$ref` in `properties` of schemas. Example)\n\n```yaml\npaths:\n  \"/locations\":\n    get:\n      responses:\n        '200':\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: \"#/components/schemas/Location\"\ncomponents:\n  schemas:\n    Location:\n      type: object\n      properties:\n        id:\n          type: string\n        name:\n          type: string\n        Coordinate:\n          \"$ref\": \"#/components/schemas/Coordinate\"\n    Coordinate:\n      type: object\n      properties:\n        lat:\n          type: string\n        lon:\n          type: string\n```\n\nNote that automatic `schemas` update feature is still new and may not work in complex scenario.\nIf you find a room for improvement, open an issue.\n\n### How can I add information which can't be generated from RSpec?\n\nrspec-openapi tries to preserve manual modifications as much as possible when generating specs.\nYou can directly edit `doc/openapi.yaml` as you like without spoiling the automatic generation capability.\n\n### Can I exclude specific specs from OpenAPI generation?\n\nYes, you can specify `openapi: false` to disable the automatic generation.\n\n```rb\nRSpec.describe '/resources', type: :request, openapi: false do\n  # ...\nend\n\n# or\n\nRSpec.describe '/resources', type: :request do\n  it 'returns a resource', openapi: false do\n    # ...\n  end\nend\n```\n\n## Customizations\n\nSome examples' attributes can be overwritten via RSpec metadata options. Example:\n\n```rb\n  describe 'GET /api/v1/posts', openapi: {\n    summary: 'list all posts',\n    description: 'list all posts ordered by pub_date',\n    tags: %w[v1 posts],\n    required_request_params: %w[limit],\n    security: [{\"MyToken\" =\u003e []}],\n  } do\n    # ...\n  end\n```\n\n**NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.\n\n### Enum Support\n\nYou 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:\n\n```rb\nit 'returns user status', openapi: {\n  enum: {\n    'status' =\u003e %w[active inactive suspended],\n  },\n} do\n  get '/users/1'\n  expect(response.status).to eq(200)\nend\n```\n\nThis generates:\n\n```yaml\nschema:\n  type: object\n  properties:\n    status:\n      type: string\n      enum:\n        - active\n        - inactive\n        - suspended\n```\n\n#### Nested Paths\n\nFor nested objects, use dot notation to specify the path:\n\n```rb\nit 'returns user with role', openapi: {\n  enum: {\n    'status' =\u003e %w[active inactive],\n    'user.role' =\u003e %w[admin user guest],\n  },\n} do\n  get '/teams/1'\n  # Response: { \"status\": \"active\", \"user\": { \"name\": \"John\", \"role\": \"admin\" } }\n  expect(response.status).to eq(200)\nend\n```\n\n#### Array Items\n\nFor properties inside array items, use the array property name followed by the item property:\n\n```rb\nit 'returns items with status', openapi: {\n  enum: {\n    'items.status' =\u003e %w[pending completed failed],\n    'items.priority' =\u003e %w[high medium low],\n  },\n} do\n  get '/tasks'\n  # Response: { \"items\": [{ \"id\": 1, \"status\": \"pending\", \"priority\": \"high\" }] }\n  expect(response.status).to eq(200)\nend\n```\n\n#### Request vs Response Enums\n\nBy 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`:\n\n```rb\nit 'creates a task', openapi: {\n  request_enum: {\n    'action' =\u003e %w[create update delete],\n  },\n  response_enum: {\n    'status' =\u003e %w[pending processing completed],\n  },\n} do\n  post '/tasks', params: { action: 'create', name: 'New Task' }\n  expect(response.status).to eq(201)\nend\n```\n\n### Multiple Examples Mode\n\nYou can generate multiple named examples for the same endpoint using `example_mode`:\n\n```rb\ndescribe '#index', openapi: { example_mode: :multiple } do\n  it 'with pagination' do\n    get '/tables', params: { page: 1, per: 10 }\n    expect(response.status).to eq(200)\n  end\n\n  it 'with filter' do\n    get '/tables', params: { filter: { name: 'test' } }\n    expect(response.status).to eq(200)\n  end\nend\n```\n\nThis generates OpenAPI with multiple named examples:\n\n```yaml\nresponses:\n  '200':\n    content:\n      application/json:\n        schema: { ... }\n        examples:\n          with_pagination:\n            value: { ... }\n          with_filter:\n            value: { ... }\n```\n\nAvailable `example_mode` values:\n- `:single` (default) - generates single `example` field\n- `:multiple` - generates named `examples` with test descriptions as keys\n- `:none` - generates only schema, no examples\n\nThe mode is inherited by nested contexts and can be overridden at any level.\n\n**Note:** If multiple examples resolve to the same example key for a single endpoint, the last one wins (overwrites).\n\n#### Merge Behavior with Mixed Modes\n\nWhen multiple tests target the same endpoint with different `example_mode` settings (even from different spec files), the merger automatically converts to `examples` format:\n\n```rb\n# spec/requests/api_spec.rb\ndescribe 'GET /users' do\n  it 'returns users' do  # default :single mode\n    get '/users'\n    expect(response.status).to eq(200)\n  end\nend\n\n# spec/requests/admin_spec.rb\ndescribe 'GET /users', openapi: { example_mode: :multiple } do\n  it 'with admin privileges' do\n    get '/users', headers: { 'X-Admin': 'true' }\n    expect(response.status).to eq(200)\n  end\nend\n```\n\nResult - both examples merged into `examples`:\n```yaml\nresponses:\n  '200':\n    content:\n      application/json:\n        examples:\n          returns_users:\n            value: { ... }\n          with_admin_privileges:\n            value: { ... }\n```\n\nTo exclude specific tests from example generation, use `example_mode: :none`:\n\n```rb\ndescribe 'GET /users', openapi: { example_mode: :none } do\n  it 'edge case test' do\n    # This won't add examples to OpenAPI spec\n  end\nend\n```\n\n## Experimental minitest support\n\nEven if you are not using `rspec` this gem might help you with its experimental support for `minitest`.\n\nExample:\n\n```rb\nclass TablesTest \u003c ActionDispatch::IntegrationTest\n  openapi!\n\n  test \"GET /index returns a list of tables\" do\n    get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }\n    assert_response :success\n  end\n\n  test \"GET /index does not return tables if unauthorized\" do\n    get '/tables'\n    assert_response :unauthorized\n  end\n\n  # ...\nend\n```\n\nIt 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.\n\nPlease 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.\n\nRun minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.\n\n```bash\n$ OPENAPI=1 bundle exec rails t\n```\n\n## Links\n\nExisting RSpec plugins which have OpenAPI integration:\n\n* [zipmark/rspec\\_api\\_documentation](https://github.com/zipmark/rspec_api_documentation)\n* [rswag/rswag](https://github.com/rswag/rswag)\n* [drewish/rspec-rails-swagger](https://github.com/drewish/rspec-rails-swagger)\n\n## Acknowledgements\n\n* Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)\n* Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.\n\n\n## Releasing\n\n1. Ensure RubyGems trusted publishing is configured for this repo and gem ownership (see [Trusted publishing](https://guides.rubygems.org/trusted-publishing/)).\n2. In GitHub Actions, run the `prepare release` workflow manually. It bumps `lib/rspec/openapi/version.rb`, pushes `release/v\u003cversion\u003e` to origin, and opens a PR.\n3. Review and merge the release PR into the default branch.\n4. Create and push a tag `v\u003cversion\u003e` on the merged commit (via the GitHub UI or `git tag v\u003cversion\u003e; git push origin v\u003cversion\u003e`). Tag creation triggers the `Publish to RubyGems` workflow, which publishes the gem and creates the GitHub release notes automatically.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":["https://github.com/sponsors/k0kubun"],"categories":["Ruby","ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoego%2Frspec-openapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexoego%2Frspec-openapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoego%2Frspec-openapi/lists"}