Verquest
Verquest is a Ruby gem that offers an elegant solution for versioning API requests. It simplifies the process of defining and evolving your API schema over time, with robust support for:
- Defining versioned request structures
- Gracefully handling API versioning
- Mapping between external and internal parameter structures
- Validating parameters against JSON Schema
- Generating components for OpenAPI documentation
- Mapping error keys back to the external API structure (planned feature)
The gem is still in development. Until version 1.0, the API may change. There are some features like
oneOf
,anyOf
,allOf
that are not implemented yet.
Installation
Add this line to your application's Gemfile:
gem "verquest", "~> 0.2"
And then execute:
bundle install
Quick Start
Define a versioned API requests
Address Create Request
class AddressCreateRequest < Verquest::Base
description "Address Create Request"
additional_properties: false
version "2025-06" do # or v1 or anything you need (use a custom version_resolver if needed)
type: :string, required: true do
field :street, description: "Street address"
field :city, description: "City of residence"
field :postal_code, description: "Postal code"
field :country, description: "Country of residence"
end
end
end
User Create Request that uses the AddressCreateRequest
class UserCreateRequest < Verquest::Base
description "User Create Request"
additional_properties: false
version "2025-06" do # or v1 or anything you need (use a custom version_resolver if needed)
type: :string, required: true do
field :first_name, description: "The first name of the user", max_length: 50
field :last_name, description: "The last name of the user", max_length: 50
field :email, format: "email", description: "The email address of the user"
end
field :birth_date, type: :string, format: "date", description: "The birth date of the user"
reference :address, from: AddressCreateRequest, required: true
collection :permissions, description: "Permissions associated with the user" do
field :name, type: :string, required: true, description: "Name of the permission"
type: :boolean do
field :read, description: "Permission to read"
field :write, description: "Permission to write"
end
end
field :role, type: :string, description: "Role of the user", enum: %w[member manager], default: "member"
object :profile_details do
field :bio, type: :string, description: "Short biography of the user"
array :hobbies, type: :string, description: "Tags associated with the user"
object :social_links, description: "Some social networks" do
type: :string, format: "uri" do
field :github, description: "GitHub profile URL"
field :mastodon, description: "Mastodon profile URL"
end
end
end
end
end
Example usage in Rails Controller
class UsersController < ApplicationController
rescue_from Verquest::InvalidParamsError, with: :handle_invalid_params
def create
result = Users::Create.call(params: user_params) # service object to handle the creation logic
if result.success?
# render success response
else
# render error response
end
end
private
def user_params
UserCreateRequest.process(params, version: params[:api_version])
end
end
JSON schema for OpenAPI
You can generate JSON Schema for your versioned requests, which can be used for API documentation:
UserCreateRequest.to_schema(version: "2025-06")
Output:
{
type: :object,
description: "User Create Request",
required: [:first_name, :last_name, :email, :address],
properties: {
first_name: {type: :string, description: "The first name of the user", maxLength: 50},
last_name: {type: :string, description: "The last name of the user", maxLength: 50},
email: {type: :string, format: "email", description: "The email address of the user"},
birth_date: {type: :string, format: "date", description: "The birth date of the user"},
address: {"$ref": "#/components/schemas/AddressCreateRequest"},
permissions: {
type: :array,
items: {
type: :object,
required: [:name],
properties: {
name: {type: :string, description: "Name of the permission"},
read: {type: :boolean, description: "Permission to read"},
write: {type: :boolean, description: "Permission to write"}
}
},
description: "Permissions associated with the user"
},
role: {type: :string, description: "Role of the user", enum: ["member", "manager"], default: "member"},
profile_details: {
type: :object,
required: [],
properties: {
bio: {type: :string, description: "Short biography of the user"},
hobbies: {type: :array, items: {type: :string}, description: "Tags associated with the user"},
social_links: {
type: :object,
required: [],
properties: {
github: {type: :string, format: "uri", description: "GitHub profile URL"},
mastodon: {type: :string, format: "uri", description: "Mastodon profile URL"}
},
description: "Some social networks"
}
}
}
},
additionalProperties: false
}
JSON schema for validation
You can check the validation JSON schema for a specific version of your request:
UserCreateRequest.to_validation_schema(version: "2025-06")
Output:
{
type: :object,
description: "User Create Request",
required: [:first_name, :last_name, :email, :address],
properties: {
first_name: {type: :string, description: "The first name of the user", maxLength: 50},
last_name: {type: :string, description: "The last name of the user", maxLength: 50},
email: {type: :string, format: "email", description: "The email address of the user"},
birth_date: {type: :string, format: "date", description: "The birth date of the user"},
address: { # from the AddressCreateRequest
type: :object,
description: "Address Create Request",
required: [:street, :city, :postal_code, :country],
properties: {
street: {type: :string, description: "Street address"},
city: {type: :string, description: "City of residence"},
postal_code: {type: :string, description: "Postal code"},
country: {type: :string, description: "Country of residence"}
},
additionalProperties: false
},
permissions: {
type: :array,
items: {
type: :object,
required: [:name],
properties: {
name: {type: :string, description: "Name of the permission"},
read: {type: :boolean, description: "Permission to read"},
write: {type: :boolean, description: "Permission to write"}
}
},
description: "Permissions associated with the user"
},
role: {type: :string, description: "Role of the user", enum: ["member", "manager"], default: "member"},
profile_details: {
type: :object,
required: [],
properties: {
bio: {type: :string, description: "Short biography of the user"},
hobbies: {type: :array, items: {type: :string}, description: "Tags associated with the user"},
social_links: {
type: :object,
required: [],
properties: {
github: {type: :string, format: "uri", description: "GitHub profile URL"},
mastodon: {type: :string, format: "uri", description: "Mastodon profile URL"}
},
description: "Some social networks"
}
}
}
},
additionalProperties: false
}
You can also validate it to ensure it meets the JSON Schema standards:
UserCreateRequest.validate_schema(version: "2025-06") # => true/false
Core Features
Schema Definition and Validation
See the example above for how to define a request schema. Verquest provides a DSL to define your API requests with various component types and helper methods based on JSON Schema, which is also used in OpenAPI specification for components.
The JSON schema can be used for both validation of incoming parameters and for generating OpenAPI documentation components.
Component types
field
: Represents a scalar value (string, integer, boolean, etc.).object
: Represents a JSON object with properties.array
: Represents a JSON array with scalar items.collection
: Represents a array of objects defined manually or by a reference to another request.reference
: Represents a reference to another request, allowing you to reuse existing request structures.
Helper methods
description
: Adds a description to the request or per version.schema_options
: Allows you to set additional options for the JSON Schema, such asadditional_properties
for request or per version. All fields (exceptreference
) can be defined with options likerequired
,format
,min_lenght
,max_length
, etc. all in snake case.with_options
: Allows you to define multiple fields with the same options, reducing repetition.
Versioning
Verquest allows you to define multiple versions of your API requests, making it easy to evolve your API over time:
class UserCreateRequest < Verquest::Base
version "2025-04" do
field :name, type: :string, required: true
field :email, type: :string, format: "email", required: true
field :street, type: :string
field :city, type: :string
end
# Implicit inheritance from the previous version
version "2025-06", exclude_properties: %i[street city] do
field :phone, type: :string
# Replace street and city with a structured address object with zip
object :address do
field :street, type: :string
field :city, type: :string
field :zip, type: :string
end
end
# Disabled inheritance, `inherit` can also be set to a specific version
version "2025-08", inherit: false do
field :name, type: :string, required: true
field :email, type: :string, format: "email", required: true
field :phone, type: :string
# Replace address with a more structured version
object :address do
field :street_line1, type: :string, required: true
field :street_line2, type: :string
field :city, type: :string, required: true
field :state, type: :string
field :postal_code, type: :string, required: true
field :country, type: :string, required: true
end
end
end
Internal Verquest::VersionResolver
is then used to resolve the right version for the one specified in the call. It implements a "downgrading" strategy - when an exact version match isn't found, it returns the closest earlier version.
Example:
UserCreateRequest.process(params, version: "2025-05") # => use the defined version "2025-04"
UserCreateRequest.process(params, version: "2025-06") # => use the defined version "2025-06"
UserCreateRequest.process(params, version: "2025-07") # => use the closest earlier version "2025-06"
UserCreateRequest.process(params, version: "2025-08") # => use the defined version "2025-08"
UserCreateRequest.process(params, version: "2025-10") # => use the closest earlier version "2025-08"
This is used across all referenced requests, so if you have a UserRequest
that references an AddressCreateRequest
, it will also resolve the correct version of the AddressCreateRequest
based on the initial requested version (as the AddressCreateRequest
can have different versions defined).
The goal here is to avoid redefining the same request structure in multiple versions when there are no changes, and to facilitate the easy evolution of API requests over time. When a new API version is created and there are no changes to the requests, you don't need to update anything.
Mapping request structure
Verquest's mapping system allows transforming external API request structures into your internal application data structures.
Here’s a short example: we store the address in the same table as the user internally, but the API request structure is different.
class UserCreateRequest < Verquest::Base
version "2025-06", exclude_properties: %i[street city] do
field :full_name, type: :string, map: "name"
field :email, type: :string, format: "email", required: true
field :phone, type: :string
object :address do
field :street, type: :string, map: "/address_street"
field :city, type: :string, map: "/address_city"
field :postal_code, type: :string, map: "/address_zip"
end
end
end
When called with UserCreateRequest.process(params)
, the address
object will be mapped to the internal structure with keys address_street
, address_city
, and address_zip
.
Example request params
{
"full_name": "John Doe",
"email": "[email protected]",
"phone": "1234567890",
"address": {
"street": "123 Main St",
"city": "Springfield",
"postal_code": "12345"
}
}
Will be transformed to:
{
"name": "John Doe",
"email": "[email protected]",
"phone": "1234567890",
"address_street": "123 Main St",
"address_city": "Springfield",
"address_zip": "12345"
}
What you can use:
/
to reference the root of the request structurenested.structure
use dot notation to reference nested structures- if the
map
is not set, the field name will be used as the key in the internal structure
There are some limitations and the implementation can be improved, but it should works for most common use cases.
See the mapping test (in test/verquest/base_test.rb
) for more examples of mapping.
Component Generation for OpenAPI
Generate JSON Schema, component name and reference for OpenAPI documentation:
UserCreateRequest.component_name # => "UserCreateRequest"
UserCreateRequest.to_ref # => "#/components/schemas/UserCreateRequest"
component_schema = UserCreateRequest.to_schema(version: "2025-06")
Configuration
Configure Verquest globally:
Verquest.configure do |config|
# Enable validation by default
config.validate_params = true # default
# Set the default version to use
config.current_version = -> { Current.api_version }
# Set the JSON Schema version
config.json_schema_version = :draft6 # default
# Set the error handling strategy for processing params
config.validation_error_handling = :raise # default, can be set also to :result
# Remove extra root keys from provided params
config.remove_extra_root_keys = true # default
# Set custom version resolver
config.version_resolver = CustomeVersionResolver # default is `Verquest::VersionResolver`
end
Documentation
For detailed documentation, please visit the YARD documentation.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in gem_version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/CiTroNaK/verquest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Verquest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.