A collection of middleware to help build services with JSON Schema, OpenAPI 2, OpenAPI 3.
Supported Ruby Versions
Committee is tested on the following MRI versions:
- 2.7
- 3.0
- 3.1
- 3.2
- 3.3
Committee::Middleware::RequestValidation
This feature is supported by all of Hyper-Schema, OpenAPI 2, and OpenAPI 3.
use Committee::Middleware::RequestValidation, schema_path: 'docs/schema.json', coerce_date_times: true
This piece of middleware validates the parameters of incoming requests to make sure that they're formatted according to the constraints imposed by a particular schema.
Configuration Options
name | Hyper-Schema | OpenAPI 3 | Description |
---|---|---|---|
allow_blank_structures | false | always true | Allow Empty Response Body. supported on Hyper-schema parser but will default to true in next major version. |
allow_form_params | true | true | Specifies that input can alternatively be specified as application/x-www-form-urlencoded parameters when possible. This won't work for more complex schema validations. |
allow_get_body | true | false | Allow GET request body, which merge to request parameter. See (#211) |
allow_query_params | true | true | Specifies that query string parameters will be taken into consideration when doing validation. |
check_content_type | true | true | Specifies that Content-Type should be verified according to JSON Hyper-schema or OpenAPI 3 definition. |
check_header | true | true | Check header data using JSON Hyper-schema or OpenAPI 3 definition. |
coerce_date_times | false | true | Convert the string with "format": "date-time" parameter to DateTime object. |
coerce_form_params | false | true | Tries to convert POST data encoded into an application/x-www-form-urlencoded body (where values are all strings) into concrete types required by the schema. This works for null (empty value), integer (numeric value without decimals), number (numeric value) and boolean ("true" is converted to true and "false" to false ). If coercion is not possible, the original value is passed unchanged to schema validation. |
coerce_path_params | false | true | The same as coerce_form_params , but tries to coerce parameters encoded in a request's URL path. |
coerce_query_params | false | true | The same as coerce_form_params , but tries to coerce GET parameters encoded in a request's query string. |
coerce_recursive | false | always true | Coerce data in arrays and other nested objects |
optimistic_json | false | false | Will attempt to parse JSON in the request body even without a Content-Type: application/json before falling back to other options. |
raise | false | false | Raise an exception on error instead of responding with a generic error body. |
strict | false | false | Puts the middleware into strict mode, meaning that paths which are not defined in the schema will be responded to with a 404 instead of being run. |
strict_reference_validation | always false | false | Raises an exception (OpenAPIParser::MissingReferenceError ) on middleware load if the provided schema file contains unresolvable references ($ref:"#/something/not/here" ). Not supported on Hyper-schema parser. Defaults to false on OpenAPI3 but will default to true in next major version. |
ignore_error | false | false | Validate and ignore result even if validation is error. So always return original data. |
Non-boolean options:
name | allowed object type | Hyper-Schema | OpenAPI 3 | Description |
---|---|---|---|---|
error_class | StandardError | supported | supported | Change validation errors from Committee::ValidationError ). |
prefix | String | supported | supported | Mounts the middleware to respond at a configured prefix. (e.g. prefix is '/v1' and request path is '/v1/test' use '/test' definition). |
schema_path | String | supported | supported | Defines the location of the schema file to use for validation. |
error_handler | Proc Object | supported | supported | A proc which will be called when error occurs. Take an Error instance as first argument, and request.env as second argument. (e.g. -> (ex, env) { Raven.capture_exception(ex, extra: { rack_env: env }) } ) |
accept_request_filter | Proc Object | supported | supported | A proc that accepts a Request and returns a boolean. It indicates whether to validate the current request, or not. (e.g. -> (request) { request.path.start_with?('/something') } ) |
params_key | String | supported | supported | Save checked and merged parameter value to request.env using this key. Default value is committee.params
|
headers_key | String | supported | supported | Save checked header value to request.env using this key. Default value is committee.headers
|
query_hash_key | String | supported | supported | Save checked query parameter value to request.env using this key. Default value is rack.request.query_hash but we will change committee.query_hash in next version |
path_hash_key | String | supported | supported | Save checked path parameter value to request.env using this key. Default value is committee.path_hash
|
request_body_hash_key | String | supported | supported | Save checked request body parameter (json, form) value to request.env using this key. Default value is committee.request_body_hash
|
Note that Hyper-Schema and OpenAPI 2 get the same defaults for options.
Some examples of use:
# missing required parameter
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api"}'
{"id":"bad_request","message":"#/paths/~1account~1app-transfers/post/requestBody/content/application~1json/schema missing required parameters: recipient"}
# missing required parameter (should have &query=...)
$ curl -X GET http://localhost:9292/search?category=all
{"id":"bad_request","message":"#/paths/~1search/get missing required parameters: query"}
# contains an unknown parameter
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":"api@heroku.com","sender":"api@heroku.com"}'
{"id":"bad_request","message":"#/paths/~1account~1app-transfers/post/requestBody/content/application~1json/schema does not define properties: sender"}
# invalid type
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":7}'
{"id":"bad_request","message":"#/paths/~1account~1app-transfers/post/requestBody/content/application~1json/schema/properties/recipient expected string, but received Integer: 7"}
# invalid format (supports date-time, email, uuid)
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":"matz"}'
{"id":"bad_request","message":"#/paths/~1account~1app-transfers/post/requestBody/content/application~1json/schema/properties/recipient email address format does not match value: matz"}
# invalid pattern
$ curl -X POST http://localhost:9292/apps -H "Content-Type: application/json" -d '{"name":"$#%"}'
{"id":"bad_request","message":"#/paths/~1apps/post/requestBody/content/application~1json/schema/properties/name pattern ^[a-z][a-z0-9-]{3,50}$ does not match value: $#%"}
Committee::Middleware::Stub
Note: This feature is not yet available for OpenAPI 3.
use Committee::Middleware::Stub, schema_path: 'docs/schema.json'
This piece of middleware intercepts any routes that are in the JSON Schema, then builds and returns an appropriate response for them.
$ curl -X GET http://localhost:9292/apps
[
{
"archived_at":"2012-01-01T12:00:00Z",
"buildpack_provided_description":"Ruby/Rack",
"created_at":"2012-01-01T12:00:00Z",
"git_url":"git@heroku.com/example.git",
"id":"01234567-89ab-cdef-0123-456789abcdef",
"maintenance":false,
"name":"example",
"owner":[
{
"email":"username@example.com",
"id":"01234567-89ab-cdef-0123-456789abcdef"
}
],
"region":[
{
"id":"01234567-89ab-cdef-0123-456789abcdef",
"name":"us"
}
],
"released_at":"2012-01-01T12:00:00Z",
"repo_size":0,
"slug_size":0,
"stack":[
{
"id":"01234567-89ab-cdef-0123-456789abcdef",
"name":"cedar"
}
],
"updated_at":"2012-01-01T12:00:00Z",
"web_url":"http://example.herokuapp.com"
}
]
committee-stub
A bundled executable is also available to easily start up a server that will serve the stub for some particular JSON Schema file:
committee-stub -p <port> <path to JSON schema>
Committee::Middleware::ResponseValidation
This feature is supported by all of Hyper-Schema, OpenAPI 2, and OpenAPI 3.
use Committee::Middleware::ResponseValidation, schema_path: 'docs/schema.json'
This piece of middleware validates the contents of the response received from up the stack for any route that matches the JSON Schema. A hyper-schema link's targetSchema
property is used to determine what a valid response looks like.
Configuration Options
name | Hyper-Schema | OpenAPI 3 | Description |
---|---|---|---|
raise | false | false | Raise an exception on error instead of responding with a generic error body. |
validate_success_only | true | false | Also validate non-2xx responses only. |
ignore_error | false | false | Validate and ignore result even if validation is error. So always return original data. |
parse_response_by_content_type | true | true | Parse response body to JSON only if Content-Type header is 'application/json'. When false, this always optimistically parses as JSON without checking for Content-Type header. |
strict | false | false | Puts the middleware into strict mode, meaning that response code and content type does not defined in the schema will be responded to with a 500 instead of application's status code. |
strict_reference_validation | always false | false | Raises an exception (OpenAPIParser::MissingReferenceError ) on middleware load if the provided schema file contains unresolvable references ($ref:"#/something/not/here" ). Not supported on Hyper-schema parser. Defaults to false on OpenAPI3 but will default to true in next major version. |
No boolean option values:
name | allowed object type | Hyper-Schema | OpenAPI 3 | Description |
---|---|---|---|---|
prefix | String | support | support | Mounts the middleware to respond at a configured prefix. |
error_class | StandardError | support | support | Specifies the class to use for formatting and outputting validation errors (defaults to Committee::ValidationError ). |
error_handler | Proc Object | support | support | A proc which will be called when error occurs. Take an Error instance as first argument, and request.env as second argument. (e.g. -> (ex, env) { Raven.capture_exception(ex, extra: { rack_env: env }) } ) |
Given a simple Sinatra app that responds for an endpoint in an incomplete fashion:
require "committee"
require "sinatra"
use Committee::Middleware::ResponseValidation, schema_path: 'docs/schema.json'
get "/apps" do
content_type :json
"[{}]"
end
The middleware will raise an error to indicate what the problems are:
# missing keys in response
$ curl -X GET http://localhost:9292/apps
{"id":"invalid_response","message":"Missing keys in response: archived_at, buildpack_provided_description, created_at, git_url, id, maintenance, name, owner:email, owner:id, region:id, region:name, released_at, repo_size, slug_size, stack:id, stack:name, updated_at, web_url."}
If you want to take log only (for example avoiding false-positive in production), use ignore_error
and error_handler
option.
Validation Errors
Committee will by default respond with a generic error JSON body for validation errors (when the raise
middleware option is false
).
Here's an example error to show the default format:
{
"id":"invalid_response",
"message":"Missing keys in response: archived_at, buildpack_provided_description, created_at, git_url, id, maintenance, name, owner:email, owner:id, region:id, region:name, released_at, repo_size, slug_size, stack:id, stack:name, updated_at, web_url."
}
You can customize this JSON body by setting the error_class
middleware option. The error_class
will be instantiated with: status
, id
, and message
.
-
status
: HTTP status code -
id
: HTTP status name/string -
message
: error message
Here's an example of a class to format errors according to JSON API:
module MyAPI
class ValidationError < Committee::ValidationError
def error_body
{
errors: [
{ status: id, detail: message }
]
}
end
def render
[
status,
{ "Content-Type" => "application/vnd.api+json" },
[JSON.generate(error_body)]
]
end
end
end
Test Assertions
Supported in HyperSchema and OpenAPI 3.
Committee ships with a small set of schema validation test assertions designed to be used along with rack-test
.
Here's a simple test to demonstrate:
describe Committee::Middleware::Stub do
include Committee::Test::Methods
include Rack::Test::Methods
def app
Sinatra.new do
get "/" do
content_type :json
JSON.generate({ "foo" => "bar" })
end
end
end
def committee_options
@committee_options ||= { schema: Committee::Drivers::load_from_file('docs/schema.json'), prefix: "/v1" }
end
def request_object
last_request
end
def response_data
[last_response.status, last_response.headers, last_response.body]
end
describe "GET /" do
it "conforms to schema with 200 response code" do
get "/"
assert_schema_conform(200)
end
it "conforms to request schema" do
get "/"
assert_request_schema_confirm
end
it "conforms to response schema with 200 response code" do
get "/"
assert_response_schema_confirm(200)
end
it "conforms to response and request schema with 200 response code" do
@committee_options[:old_assert_behavior] = false
get "/"
assert_schema_conform(200)
end
end
end
Tips
Use Ruby on Rails with coerce option
Please set 'action_dispatch.request.request_parameters'
to params_key
option.
use Committee::Middleware::RequestValidation,
schema_path: 'docs/schema.json',
coerce_date_times: true,
params_key: 'action_dispatch.request.request_parameters'
Committee has few options which enable convert request data.
By default committee save converted data to committee.params
and rails does not read it.
So we need to save converted value to 'action_dispatch.request.request_parameters'
because rails create parameter from this value.
Using OpenAPI 3
Committee can detect the type of schema (Hyper-Schema, OpenAPI 3, etc.) from the provided file, so there's no need to pass in any additional options:
use Committee::Middleware::RequestValidation, schema_path: 'open_api_3/schema.yaml'
If you want to select the type manually, pass an OpenAPI 3
object to the schema
option manually:
open_api = OpenAPIParser.parse(YAML.load_file('open_api_3/schema.yaml'))
schema = Committee::Drivers::OpenAPI3::Driver.new.parse(open_api)
use Committee::Middleware::RequestValidation, schema: schema
Limitations of OpenAPI 3 support
- Stub servers are not yet supported, so neither
Committee::Middleware::Stub
orCommittee::Bin::CommitteeStub
are functional. - Changing
coerce_recursive
isn't supported. This option is always on.
Upgrading from Committee 2.* to 3.*
Committee 3.* has many breaking changes so we recommend upgrading to the latest release on 2.* and fixing any deprecation errors you see before upgrading to 3.*. The steps would be roughly as follows:
- Update to the latest 2.* release (usually by modifying the statement in your
Gemfile
and runningbundle update
). - Run your test suite and fix any deprecation warnings that appear.
- Update to the latest 3.* release.
- Switch to OpenAPI 3 if you'd like to do so.
Important changes are also described below.
Upgrading from Committee 4.* to 5.*
Committee 5.* has few breaking changes so we recommend upgrading to the latest release on 4.* and fixing any deprecation errors you see before upgrading.
- set
parse_response_by_content_type=true
by default (old versions setfalse
) - set
parameter_overwrite_by_rails_rule=true
by default (old version setfalse
)
Future Updates (5.*~)
OpenAPI3 Schema Users: Newly-added strict_reference_validation
option defaults to false
if not set.
From a later major version of Committee, we want to change it to true
(or remove the option entirely and force
true
to be passed to the OpenAPI3 parser (see: #343).
To remove deprecation warnings, pass strict_reference_validation: true
as an option when loading the middleware
(because this is about schema parsing/initialization, the setting is valid for both request and response validation).
Setting schemas in middleware
Committee 2.* supported setting schema
to a string or a hash like this:
# valid
use Committee::Middleware::RequestValidation, schema: JSON.parse(File.read(...))
# valid
use Committee::Middleware::RequestValidation, schema: {json: 'json_data...'}
# valid
use Committee::Middleware::RequestValidation, schema: 'json string'
That usage is no longer supported in 3.* Instead, use either schema_path
or set a parsed schema object to schema
:
# auto-select Hyper-Schema/OpenAPI 2/OpenAPI 3 from file
use Committee::Middleware::RequestValidation, schema_path: 'docs/schema.json' # using file extension
# auto-select Hyper-Schema/OpenAPI 2/OpenAPI 3 from hash
json = JSON.parse(File.read('docs/schema.json'))
use Committee::Middleware::RequestValidation, schema: Committee::Drivers::load_data(json)
# manually select
json = JSON.parse(File.read(...))
schema = Committee::Drivers::HyperSchema::Driver.new.parse(json)
use Committee::Middleware::RequestValidation, schema: schema
The auto-select algorithm works roughly like this (so make sure that your file sets one of these attributes correctly):
hash = JSON.load(json_path)
# OpenAPI 3 requires the `openapi` key and a version
if hash['openapi']&.start_with?('3.')
return Committee::Drivers::OpenAPI3::Driver.new.parse(hash)
# OpenAPI 2 requires the `swagger` key
elsif hash['swagger'] == '2.0'
return Committee::Drivers::OpenAPI2::Driver.new.parse(hash)
else
return Committee::Drivers::HyperSchema::Driver.new.parse(hash)
end
Test assertions
Committee 3.* drops many of the methods that were
previously available from the Committee::Test::Methods
mixin.
Use it by defining a committee_options
method and having
it return a schema and other options you'd like to use:
def committee_options
@committee_options ||= { schema: Committee::Drivers::load_from_file('docs/schema.json'), prefix: "/v1", validate_success_only: true }
end
The default assertion option in 2.* was validate_success_only=true
, but this becomes validate_success_only=false
in 3.. For the smoothest possible upgrade, you should set it to false
in your test suite before upgrading to 3..
Test schema coverage
You can check how much of your API schema your tests have covered.
NOTICE: Currently committee only supports schema coverage for openapi schemas, and only checks coverage on responses, via assert_response_schema_confirm
or assert_schema_conform
methods.
Usage:
- Set schema_coverage option of
committee_options
- Use
assert_response_schema_confirm
orassert_schema_conform
- Then use
SchemaCoverage#report
orSchemaCoverage#report_flatten
to get coverage report
Example:
before do
schema_coverage = Committee::Test::SchemaCoverage.new(openapi_schema)
@committee_options[:schema_coverage] = schema_coverage
end
it 'covers /some_api' do
get '/some_api'
assert_response_schema_confirm # or assert_schema_conform
coverage_report = schema_coverage.report
# check coverage expectations of /some_api here
end
it 'covers /other_api schema' do
get '/other_api'
assert_response_schema_confirm # or assert_schema_conform
coverage_report = schema_coverage.report
# check coverage expectations of /other_api here
end
after do
coverage_report = schema_coverage.report
# check coverage expectations of all apis here
end
Coverage report structure:
/* using #report */
{
<path> => {
<method> => {
'responses' => {
<status> => <true|false>
}
}
}
}
/* using #report_flatten */
{
responses: [
{ path: <path>, method: <method>, status: <status>, is_covered: <true|false> },
]
}
Other helper methods:
-
Committee::Test::SchemaCoverage.merge_report(<Hash>, <Hash>)
: merge 2 coverage reports together -
Committee::Test::SchemaCoverage.flatten_report(<Hash>)
: flatten a coverage report Hash into flatten structure
Other changes
-
GET
request bodies are ignored in OpenAPI 3 by default. If you want to use them, set theallow_get_body
option totrue
.
Development
Run tests with the following:
bundle install
bundle exec rake
Run a particular test suite or test:
bundle exec ruby -Ilib -Itest test/router_test.rb
bundle exec ruby -Ilib -Itest test/router_test.rb -n /prefix/
Release
-
Update the version in
lib/committee/version.rb
as appropriate for semantic versioning and add details toCHANGELOG.md
. -
Commit those changes. Use a commit message like
Bump version to 1.2.3
. -
Run the
release
task:bundle exec rake release