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 extend your 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
      tags '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:

  1. Reads the contract file as plain text using the contract's source location
  2. Parses to AST using Ruby's built-in Ripper parser
  3. Walks the AST to find the params do...end block
  4. Extracts field definitions (required/optional, names, types, nesting)
  5. 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.