DryValidationOpenapi
Automatically generate OpenAPI 3.0 schemas from dry-validation contracts for use with rswag and other OpenAPI tools.
Instead of manually writing OpenAPI schemas for your API documentation, this gem extracts type information directly from your dry-validation contract definitions using static parsing.
Features
- 🎯 Zero runtime overhead - Uses static parsing, no contract instantiation needed
- 🔄 Automatic conversion - Converts dry-validation types to OpenAPI types with formats
- 🪆 Nested schemas - Handles nested objects and arrays of objects
- 📝 Simple API - Just
extendyour contract and call.open_api_schema - ✅ Type formats - Includes OpenAPI format specifications (int32, double, date-time, etc.)
Supported Features
✅ Currently Supported
- [x] Required and optional fields
- [x] Basic types: string, integer, float, decimal, boolean
- [x] Type formats: int32, double, float, date, date-time
- [x] Array types with item schemas
- [x] Nested objects/hashes with properties
- [x] Arrays of objects with nested schemas
- [x] Multiple nested objects at the same level
- [x]
.value()and.filled()predicates
📋 Not Yet Implemented
- [ ] Custom type formats (email, uuid, etc.)
- [ ] Descriptions and examples
- [ ] Min/max constraints
- [ ] Enums
- [ ] Custom rules/validations
- [ ] Pattern validations
- [ ] Maybe/nil handling
Installation
Add this line to your application's Gemfile:
gem 'dry_validation_openapi'
And then execute:
bundle install
Or install it yourself as:
gem install dry_validation_openapi
Usage
Basic Usage
require 'dry_validation_openapi'
class CreateUserContract < Dry::Validation::Contract
extend DryValidationOpenapi::Convertable
params do
required(:email).value(:string)
required(:age).value(:integer)
optional(:name).value(:string)
end
end
# Generate OpenAPI schema
schema = CreateUserContract.open_api_schema
# => {
# type: :object,
# properties: {
# email: { type: :string },
# age: { type: :integer, format: 'int32' },
# name: { type: :string }
# },
# required: ['email', 'age']
# }
With rswag
Use in your rswag request specs:
# spec/requests/users_spec.rb
require 'swagger_helper'
RSpec.describe 'Users API' do
path '/users' do
post 'Creates a user' do
'Users'
consumes 'application/json'
parameter name: :body,
in: :body,
required: true,
schema: CreateUserContract.open_api_schema
response '201', 'user created' do
let(:body) { { email: '[email protected]', age: 25 } }
run_test!
end
end
end
end
Nested Objects
class CreateOrderContract < Dry::Validation::Contract
extend DryValidationOpenapi::Convertable
params do
required(:order_id).value(:string)
required(:customer).hash do
required(:name).value(:string)
required(:email).value(:string)
end
end
end
CreateOrderContract.open_api_schema
# => {
# type: :object,
# properties: {
# order_id: { type: :string },
# customer: {
# type: :object,
# properties: {
# name: { type: :string },
# email: { type: :string }
# },
# required: ['name', 'email']
# }
# },
# required: ['order_id', 'customer']
# }
Arrays of Objects
class CreateInvoiceContract < Dry::Validation::Contract
extend DryValidationOpenapi::Convertable
params do
required(:issue_date).value(:time)
required(:line_items).array(:hash) do
required(:description).filled(:string)
required(:amount).filled(:decimal)
end
end
end
CreateInvoiceContract.open_api_schema
# => {
# type: :object,
# properties: {
# issue_date: { type: :string, format: 'date-time' },
# line_items: {
# type: :array,
# items: {
# type: :object,
# properties: {
# description: { type: :string },
# amount: { type: :number, format: 'double' }
# },
# required: ['description', 'amount']
# }
# }
# },
# required: ['issue_date', 'line_items']
# }
Type Mappings
| dry-validation type | OpenAPI type | OpenAPI format |
|---|---|---|
:string |
string |
- |
:integer, :int |
integer |
int32 |
:float |
number |
float |
:decimal, :number |
number |
double |
:bool, :boolean |
boolean |
- |
:date |
string |
date |
:time, :date_time |
string |
date-time |
:hash |
object |
- |
:array |
array |
- |
How It Works
This gem uses static parsing rather than runtime introspection:
- Reads the contract file as plain text using the contract's source location
- Parses to AST using Ruby's built-in Ripper parser
- Walks the AST to find the
params do...endblock - Extracts field definitions (required/optional, names, types, nesting)
- Converts to OpenAPI schema format
This approach is:
- ✅ Fast and lightweight (no contract instantiation)
- ✅ Simple to understand (just parsing Ruby code)
- ✅ Works without dry-validation loaded
- ⚠️ Cannot handle dynamically-generated contracts (rare in practice)
Development
After checking out the repo, run:
bundle install
Run the tests:
bundle exec rspec
Build the gem:
gem build dry_validation_openapi.gemspec
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/troptropcontent/dry_validation_openapi.
License
The gem is available as open source under the terms of the MIT License.
Credits
Created to solve the problem of maintaining duplicate schema definitions in dry-validation contracts and OpenAPI documentation.