grape_openapi3
Generate OpenAPI 3.0 documentation directly from your Grape API — no conversion, no middleman, no grape-swagger required.
The gem reads your routes, params do blocks, desc options, and Grape::Entity classes and produces a valid openapi.json in one call.
How it works
Most solutions convert Swagger 2.0 → OpenAPI 3.0. This gem skips that entirely.
It reads your Grape API natively:
Your Grape API
└── routes → paths + operationId
└── params do → requestBody / query params
└── desc(...) → summary, description, tags, responses
└── Grape::Entity → components/schemas with $ref
↓
openapi.json ✅
Zero runtime dependencies. Grape and grape-entity are already in your app.
Installation
Add to your Gemfile:
gem "grape_openapi3"
Then:
bundle install
Quick start
Call GrapeOpenapi3.generate passing your Grape API class:
require "grape_openapi3"
require "json"
doc = GrapeOpenapi3.generate(
V2::ApiGrape,
info: {
title: "My API",
version: "v2",
description: "API documentation",
},
servers: [
{ url: "https://api.example.com/api/v2", description: "Production" },
{ url: "http://localhost:3000/api/v2", description: "Development" },
],
)
File.write("public/openapi.json", JSON.pretty_generate(doc))
That's it. Open public/openapi.json and you have a valid OpenAPI 3.0 document.
Documenting your endpoints
Basic desc
desc "List all products", {
success: { code: 200, model: Entities::ProductListEntity, message: "Products returned." },
failure: [
{ code: 401, message: "Unauthorized" },
],
detail: "Returns a paginated list. Supports filtering by name and category.",
tags: ["products"],
params: {
page: { type: Integer, desc: "Page number (default: 1)" },
per_page: { type: Integer, desc: "Items per page (max: 100)" },
search: { type: String, desc: "Filter by name" },
active: { type: Grape::API::Boolean, desc: "Filter by active status" },
}
}
get do
# ...
end
Success options
# Just the entity — gem picks the HTTP status automatically (POST→201, GET→200, DELETE→204)
success: Entities::ProductEntity
# With explicit code + message
success: { code: 200, message: "Done." }
# Full form: code + entity + message
success: { code: 201, model: Entities::ProductEntity, message: "Product created." }
Params via params do block
params do
requires :name, type: String, desc: "Product name"
requires :price, type: Float, desc: "Price in USD"
optional :active, type: Grape::API::Boolean, desc: "Active status"
end
post do
# ...
end
Both styles (desc params: hash and params do block) are supported and can be mixed.
Response schemas with Grape::Entity
Entities are automatically converted to components/schemas with $ref:
class ProductEntity < Grape::Entity
expose :id, documentation: { type: Integer, desc: "Product ID", required: true }
expose :name, documentation: { type: String, desc: "Product name", required: true }
expose :description, documentation: { type: String, desc: "Description", nullable: true }
expose :price, documentation: { type: Float, desc: "Price in USD", required: true }
expose :active, documentation: { type: :boolean, desc: "Active status" }
end
Nested entities with using: are picked up automatically and generate their own $ref:
class OrderEntity < Grape::Entity
expose :id, documentation: { type: Integer, desc: "Order ID" }
expose :product, using: ProductEntity,
documentation: { desc: "The ordered product", nullable: true }
end
Array responses via is_array: true:
class ProductListEntity < Grape::Entity
expose :data, using: ProductEntity,
documentation: { type: ProductEntity, is_array: true, desc: "Products", required: true }
expose :total, documentation: { type: Integer, desc: "Total records", required: true }
end
Authentication / Security
doc = GrapeOpenapi3.generate(
V2::ApiGrape,
info: { title: "My API", version: "v2" },
servers: [{ url: "https://api.example.com/api/v2" }],
security_schemes: {
Bearer: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description: "Pass your JWT token in the Authorization header.",
},
},
security: [{ Bearer: [] }],
)
When security is set, a 401 Unauthorized response is automatically added to every endpoint. Routes that explicitly set security: [] are treated as public and skip the 401.
operationId
Every operation gets a unique operationId automatically derived from the HTTP method and path:
| Method | Path | operationId |
|---|---|---|
| GET | /products |
listProducts |
| POST | /products |
createProduct |
| GET | /products/{id} |
getProduct |
| PUT | /products/{id} |
updateProduct |
| DELETE | /products/{id} |
deleteProduct |
| GET | /products/{id}/images |
listProductImages |
This makes Postman imports, SDK generators, and Redoc anchors work out of the box.
Rails integration
Generate the rake task
rails generate grape_openapi3:install "V2::ApiGrape"
This creates lib/tasks/openapi.rake in your project. Then run:
bundle exec rake openapi:generate
# Override the server URL at runtime
OPENAPI_SERVER_URL=https://api.example.com/api/v2 bundle exec rake openapi:generate
Serve Swagger UI (zero dependencies)
Create public/swagger.html:
<!DOCTYPE html>
<html>
<head>
<title>API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({ url: "/openapi.json", dom_id: "#swagger-ui" });
</script>
</body>
</html>
Rails serves public/ as static files automatically. Navigate to http://localhost:3000/swagger.html.
Example project
The example/rails_app/ folder contains a full Rails 8 + Grape API with a products CRUD — the same setup described in this README, ready to run:
cd example/rails_app
bundle install
rails db:create db:migrate db:seed
rails server
# API live at:
# http://localhost:3000/api/v1/products
# Generate the docs:
bundle exec rake openapi:generate
# View Swagger UI:
# http://localhost:3000/swagger.html
Type reference
| Ruby / Grape type | OpenAPI schema |
|---|---|
String |
{ "type": "string" } |
Integer |
{ "type": "integer" } |
Float / BigDecimal |
{ "type": "number", "format": "float" } |
Date |
{ "type": "string", "format": "date" } |
DateTime / Time |
{ "type": "string", "format": "date-time" } |
Grape::API::Boolean / :boolean |
{ "type": "boolean" } |
File |
{ "type": "string", "format": "binary" } |
Hash |
{ "type": "object" } |
[String] / [Integer] |
{ "type": "array", "items": { ... } } |
License
MIT — © Rodrigo Barreto