Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9b2f763
Add API Model Layer for OpenAPI 3.x support
numbata Dec 3, 2025
4a0ce3d
Add Model Builder layer for converting routes to API Model
numbata Dec 3, 2025
7705397
Add Exporters for Swagger 2.0, OpenAPI 3.0, and 3.1 output
numbata Dec 3, 2025
27b2e9a
Integrate OpenAPI 3.x support with configuration option
numbata Dec 3, 2025
7d8ef4e
Fix requestBody content generation for OAS 3.x
numbata Dec 3, 2025
efbe285
Document openapi_version configuration option
numbata Dec 3, 2025
2c05c92
Add comprehensive OAS 3.x tests and fix security/empty array handling
numbata Dec 3, 2025
e5c379c
Add formData to requestBody conversion for OAS 3.x
numbata Dec 3, 2025
2a6bc9f
Fix reference_entity_spec.rb test expectations
numbata Dec 3, 2025
1d9430b
Add OAS 3.1 specific features
numbata Dec 3, 2025
7a23a64
Fix test expectations for issues 539 and 962
numbata Dec 3, 2025
bcb2e4f
Add comprehensive OpenAPI 3.x integration tests
numbata Dec 3, 2025
62f840f
Add OAS 3.1 configuration options: jsonSchemaDialect and webhooks
numbata Dec 3, 2025
ec6d3f2
Document OAS 3.1 configuration options in README
numbata Dec 3, 2025
136ab3c
Add OAS 3.1 type: null support
numbata Dec 3, 2025
a84e316
Add OAS 3.0 requestBody param_type: body tests
numbata Dec 3, 2025
0bafe1d
Add OAS 3.0 params array spec
numbata Dec 3, 2025
913c4f0
Add OAS 3.0 type format spec and fix JSON type mapping
numbata Dec 3, 2025
45b310b
Fix rubocop offenses on added files
numbata Dec 3, 2025
5c87e80
Refactor complex methods to fix Metrics rubocop offenses
numbata Dec 4, 2025
81b64a7
Add OAS3 tests for nested body params, response models, and compositi…
numbata Dec 4, 2025
dcf563e
Add P2 OAS3 features: additional_properties, discriminator, links, ca…
numbata Dec 4, 2025
d59808d
Add nullable handling and port additional OAS3 specs
numbata Dec 4, 2025
f567358
Add OAS3 implementation documentation
numbata Dec 4, 2025
9eef83a
Enable DirectSpecBuilder for OAS3 generation with nested entity support
numbata Dec 4, 2025
f3f8fee
Update OAS3 docs to document DirectSpecBuilder as primary builder
numbata Dec 4, 2025
7712ecb
Rename ApiModel → OpenAPI and ModelBuilder → OpenAPI::Builder
numbata Dec 5, 2025
3cbcd27
Add Dry-types/Dry-validation to future enhancements
numbata Dec 5, 2025
62eda91
Clarify Dry support is a separate model parser gem
numbata Dec 5, 2025
37192b7
Remove outdated limitation comments from FromRoutes
numbata Dec 5, 2025
d050ded
Fix rubocop offenses in OpenAPI module
numbata Dec 5, 2025
5803ab5
Refactor OAS3 builders to reduce complexity and class length
numbata Dec 5, 2025
9c62392
Fix hidden properties appearing in required array
numbata Dec 6, 2025
6653e5a
Refactor DocMethods and fix consumes string handling
numbata Dec 6, 2025
66b96bb
Add Grape 3.1 compatibility for route.attributes and Route constructor
numbata Dec 6, 2025
2591d8b
Fix OAS 3.0 type format test to expect correct types
numbata Dec 6, 2025
7d68c3d
Refactor to parser-agnostic architecture
numbata Dec 6, 2025
01c3975
Fix RouteHelper to honor anchor: false option
numbata Dec 6, 2025
d5e0e27
Replace Set with Array in build_tags to avoid missing require
numbata Dec 6, 2025
831deab
Improve code quality in OpenAPI builders
numbata Dec 6, 2025
e05a10e
Refactor OpenAPI builder structure and consolidate docs
numbata Dec 7, 2025
0e6c500
Fix nullable extension for Swagger 2.0 compatibility
numbata Dec 7, 2025
0384052
Remove license identifier from Swagger 2.0 output
numbata Dec 7, 2025
9909b17
Add backward compatibility for x: { nullable: true } in OAS 3.x
numbata Dec 7, 2025
f4c2f25
Extract builder sections into separate concern modules
numbata Dec 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
- [insert_after](#insert_after)
- [CORS](#cors)
- [Configure ](#configure-)
- [openapi_version: ](#openapi_version-)
- [json_schema_dialect: (OAS 3.1 only)](#json_schema_dialect-)
- [webhooks: (OAS 3.1 only)](#webhooks-)
- [host: ](#host-)
- [base_path: ](#base_path-)
- [mount_path: ](#mount_path-)
Expand Down Expand Up @@ -130,7 +133,9 @@ The following versions of grape, grape-entity and grape-swagger can currently be

## Swagger-Spec <a name="swagger-spec"></a>

Grape-swagger generates documentation per [Swagger / OpenAPI Spec 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md).
Grape-swagger generates documentation per [Swagger / OpenAPI Spec 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) by default.

It also supports [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) and [OpenAPI 3.1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md). See the [openapi_version configuration option](#openapi_version) for details.

<!-- validating it with: http://bigstickcarpet.com/swagger-parser/www/index.html -->

Expand Down Expand Up @@ -260,6 +265,9 @@ end

## Configure <a name="configure"></a>

* [openapi_version](#openapi_version)
* [json_schema_dialect (OAS 3.1 only)](#json_schema_dialect)
* [webhooks (OAS 3.1 only)](#webhooks)
* [host](#host)
* [base_path](#base_path)
* [mount_path](#mount_path)
Expand Down Expand Up @@ -294,6 +302,114 @@ add_swagger_documentation \
```


#### openapi_version: <a name="openapi_version"></a>
Specifies which OpenAPI/Swagger version to generate. By default (`nil`), Swagger 2.0 is generated.

Available options:
- `nil` - Swagger 2.0 (default, for backward compatibility)
- `'3.0'` - OpenAPI 3.0.3
- `'3.1'` - OpenAPI 3.1.0

```ruby
# Swagger 2.0 (default)
add_swagger_documentation

# OpenAPI 3.0
add_swagger_documentation \
openapi_version: '3.0'

# OpenAPI 3.1
add_swagger_documentation \
openapi_version: '3.1'
```

Key differences when using OpenAPI 3.x:
- Body parameters are converted to `requestBody` with content type wrappers
- Schema references use `#/components/schemas/` instead of `#/definitions/`
- Parameters include a `schema` wrapper
- `host`, `basePath`, and `schemes` are converted to a `servers` array
- OpenAPI 3.1 uses `type: ["string", "null"]` instead of `nullable: true`


#### json_schema_dialect: <a name="json_schema_dialect"></a>
**OpenAPI 3.1 only.** Specifies the JSON Schema dialect used in the document. This option is ignored for OpenAPI 3.0 and Swagger 2.0.

```ruby
add_swagger_documentation \
openapi_version: '3.1',
json_schema_dialect: 'https://json-schema.org/draft/2020-12/schema'
```

This adds `jsonSchemaDialect` to the OpenAPI 3.1 output:
```json
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
...
}
```


#### webhooks: <a name="webhooks"></a>
**OpenAPI 3.1 only.** Defines webhook endpoints that your API can call. This option is ignored for OpenAPI 3.0 and Swagger 2.0.

```ruby
add_swagger_documentation \
openapi_version: '3.1',
webhooks: {
newPetAvailable: {
post: {
summary: 'New pet available',
description: 'A new pet has been added to the store',
operationId: 'newPetWebhook',
tags: ['pets'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
petId: { type: 'integer', description: 'Pet ID' },
petName: { type: 'string', description: 'Pet name' }
},
required: %w[petId petName]
}
}
}
},
responses: {
'200': { description: 'Webhook received successfully' },
'400': { description: 'Invalid payload' }
}
}
}
}
```

You can also reference existing schemas:
```ruby
webhooks: {
petCreated: {
post: {
summary: 'Pet created',
requestBody: {
required: true,
content: {
'application/json': {
schema: { '$ref': '#/components/schemas/Pet' }
}
}
},
responses: {
'200': { description: 'OK' }
}
}
}
}
```


#### host: <a name="host"></a>
Sets explicit the `host`, default would be taken from `request`.
```ruby
Expand Down
113 changes: 113 additions & 0 deletions docs/openapi_3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# OpenAPI 3.0/3.1 Support

## Quick Start

```ruby
# Swagger 2.0 (default, unchanged)
add_swagger_documentation

# OpenAPI 3.0
add_swagger_documentation(openapi_version: '3.0')

# OpenAPI 3.1
add_swagger_documentation(openapi_version: '3.1')
```

## Configuration Options

```ruby
add_swagger_documentation(
openapi_version: '3.1',
info: {
title: 'My API',
version: '1.0',
description: 'API description',
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT',
identifier: 'MIT' # OAS 3.1 only (SPDX)
}
},
security_definitions: {
bearer: { type: 'http', scheme: 'bearer' }
},
# OAS 3.1 specific
json_schema_dialect: 'https://json-schema.org/draft/2020-12/schema',
webhooks: {
newUser: {
post: {
summary: 'New user webhook',
requestBody: { ... },
responses: { '200' => { description: 'OK' } }
}
}
}
)
```

## Key Differences from Swagger 2.0

| Aspect | Swagger 2.0 | OpenAPI 3.x |
|--------|-------------|-------------|
| Version | `swagger: '2.0'` | `openapi: '3.0.3'` / `'3.1.0'` |
| Body params | `in: body` parameter | `requestBody` object |
| Form params | `in: formData` | `requestBody` with content-type |
| File upload | `type: file` | `type: string, format: binary` |
| Schema refs | `#/definitions/X` | `#/components/schemas/X` |
| Host | `host`, `basePath`, `schemes` | `servers: [{url: "..."}]` |
| Security defs | `securityDefinitions` | `components/securitySchemes` |
| Param types | inline `type`, `format` | wrapped in `schema` |

## Nullable Fields

```ruby
# Recommended syntax
params do
optional :nickname, type: String, documentation: { nullable: true }
end

# Also supported (for backward compatibility with Swagger 2.0 code)
params do
optional :nickname, type: String, documentation: { x: { nullable: true } }
end
```

Both syntaxes produce version-appropriate output:

| Syntax | Swagger 2.0 | OAS 3.0 | OAS 3.1 |
|--------|-------------|---------|---------|
| `documentation: { nullable: true }` | `x-nullable: true` | `nullable: true` | `type: ["string", "null"]` |
| `documentation: { x: { nullable: true } }` | `x-nullable: true` | `nullable: true` | `type: ["string", "null"]` |

This means existing Swagger 2.0 code using `x: { nullable: true }` works seamlessly when switching to OAS 3.x.

## Architecture

```
Grape Routes
┌─────────────────────────────┐
│ Builder::Spec │
│ (lib/grape-swagger/openapi)│
└─────────────────────────────┘
┌─────────────────────────────┐
│ OpenAPI Model Layer │
│ (Document, Schema, etc.) │
└─────────────────────────────┘
├──────────────┬──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Swagger2 │ │ OAS30 │ │ OAS31 │
│ Exporter │ │ Exporter │ │ Exporter │
└──────────┘ └──────────┘ └──────────┘
```

## Backward Compatibility

- **Default unchanged**: Without `openapi_version`, Swagger 2.0 is generated
- **All existing options work**: Same configuration for both versions
- **Model parsers unchanged**: grape-entity, representable, etc. work as before
26 changes: 26 additions & 0 deletions lib/grape-swagger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,41 @@
require 'grape-swagger/request_param_parser_registry'
require 'grape-swagger/token_owner_resolver'

# OpenAPI 3.x support
require 'grape-swagger/openapi'
require 'grape-swagger/openapi/builder'
require 'grape-swagger/exporter'

module GrapeSwagger
class << self
attr_writer :schema_name_generator

def model_parsers
@model_parsers ||= GrapeSwagger::ModelParsers.new
end

def request_param_parsers
@request_param_parsers ||= GrapeSwagger::RequestParamParserRegistry.new
end

def schema_name_generator
@schema_name_generator ||= default_schema_name_generator
end

private

def default_schema_name_generator
lambda { |model|
model_string = model.to_s
if model_string.end_with?('::Entity', '::Entities')
model_string.split('::')[0..-2].join('_')
elsif model_string.start_with?('Entity::', 'Entities::', 'Representable::')
model_string.split('::')[1..-1].join('_')
else
model_string.split('::').join('_')
end
}
end
end
autoload :Rake, 'grape-swagger/rake/oapi_tasks'

Expand Down
36 changes: 9 additions & 27 deletions lib/grape-swagger/doc_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
require 'grape-swagger/doc_methods/move_params'
require 'grape-swagger/doc_methods/build_model_definition'
require 'grape-swagger/doc_methods/version'
require 'grape-swagger/doc_methods/webhooks'
require 'grape-swagger/doc_methods/output_builder'

module GrapeSwagger
module DocMethods
Expand All @@ -37,38 +39,18 @@ module DocMethods
specific_api_documentation: { desc: 'Swagger compatible API description for specific API' },
endpoint_auth_wrapper: nil,
swagger_endpoint_guard: nil,
token_owner: nil
token_owner: nil,
# OpenAPI version: nil (Swagger 2.0), '3.0', or '3.1'
openapi_version: nil,
# OpenAPI 3.1 specific options
json_schema_dialect: nil,
webhooks: nil
}.freeze

FORMATTER_METHOD = %i[format default_format default_error_formatter].freeze

def self.output_path_definitions(combi_routes, endpoint, target_class, options)
output = endpoint.swagger_object(
target_class,
endpoint.request,
options
)

paths, definitions = endpoint.path_and_definition_objects(combi_routes, options)
tags = tags_from(paths, options)

output[:tags] = tags unless tags.empty? || paths.blank?
output[:paths] = paths unless paths.blank?
output[:definitions] = definitions unless definitions.blank?

output
end

def self.tags_from(paths, options)
tags = GrapeSwagger::DocMethods::TagNameDescription.build(paths)

if options[:tags]
names = options[:tags].map { |t| t[:name] }
tags.reject! { |t| names.include?(t[:name]) }
tags += options[:tags]
end

tags
OutputBuilder.build(combi_routes, endpoint, target_class, options)
end

def hide_documentation_path
Expand Down
10 changes: 9 additions & 1 deletion lib/grape-swagger/doc_methods/build_model_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ class << self
def build(_model, properties, required, other_def_properties = {})
definition = { type: 'object', properties: properties }.merge(other_def_properties)

definition[:required] = required if required.is_a?(Array) && required.any?
if required.is_a?(Array) && required.any?
# Filter required to only include properties that actually exist
# (handles hidden properties that are removed from properties but left in required)
property_keys = properties.keys.map(&:to_s)
valid_required = required.select { |r| property_keys.include?(r.to_s) }
definition[:required] = valid_required if valid_required.any?
end

definition
end

def parse_params_from_model(parsed_response, model, model_name)
return parsed_response.to_h if parsed_response.is_a?(GrapeSwagger::OpenAPI::Schema)

if parsed_response.is_a?(Hash) && parsed_response.keys.first == :allOf
refs_or_models = parsed_response[:allOf]
parsed = parse_refs_and_models(refs_or_models, model)
Expand Down
12 changes: 3 additions & 9 deletions lib/grape-swagger/doc_methods/data_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,9 @@ def parse_multi_type(raw_data_type)
end

def parse_entity_name(model)
if model.respond_to?(:entity_name)
model.entity_name
elsif model.to_s.end_with?('::Entity', '::Entities')
model.to_s.split('::')[0..-2].join('_')
elsif model.to_s.start_with?('Entity::', 'Entities::', 'Representable::')
model.to_s.split('::')[1..-1].join('_')
else
model.to_s.split('::').join('_')
end
return model.entity_name if model.respond_to?(:entity_name)

GrapeSwagger.schema_name_generator.call(model)
end

def request_primitive?(type)
Expand Down
Loading
Loading