rspec-json_api adds RSpec matchers for checking JSON values against compact Ruby schemas. Despite the name, it validates general JSON response shapes; it does not implement the JSON:API specification.
- Ruby 3.2 or newer
- RSpec 3
- Rails 6.1 or newer only when using the optional generators
The CI suite covers matcher behavior on Ruby 3.2, 3.3, 3.4, and 4.0. Generator integration is exercised at the supported Rails boundaries: Rails 6.1 on Ruby 3.2 and Rails 8.1 on Ruby 3.4.
Add the gem to your test group:
group :test do
gem "rspec-json_api"
endThen run:
bundle installLoad the matchers from spec/spec_helper.rb (or your equivalent RSpec setup file):
require "rspec/json_api"Rails projects can generate the definition directories:
rails generate rspec:json_api:installLoad custom types before interfaces in rails_helper.rb, because interfaces may reference types:
Dir[File.join(__dir__, "rspec", "json_api", "types", "*.rb")].each { |file| require file }
Dir[File.join(__dir__, "rspec", "json_api", "interfaces", "*.rb")].each { |file| require file }The matchers themselves do not depend on Rails, ActiveSupport, or rspec-rails.
Pass the matcher a JSON String and describe the parsed value with Ruby values, classes, regular expressions, arrays, hashes, or constraint Procs:
schema = {
id: RSpec::JsonApi::Types::UUID,
name: String,
age: -> { { type: Integer, min: 18 } },
tags: [String]
}
expect(response.body).to match_json_schema(schema)The actual value must be a JSON String. Invalid JSON and non-String inputs fail the match. Schema keys must be symbols because JSON object keys are symbolized while parsing.
Object schemas are strict at every nesting level: every expected key must be present and unexpected keys fail the match. allow_blank permits a blank value; it does not make a key optional.
Root schemas may describe objects, arrays, or scalar JSON values:
expect('"ready"').to match_json_schema(String)
expect('"ready"').to match_json_schema(/\Aready\z/)
expect("42").to match_json_schema(42)have_no_content matches only an empty String:
expect(response.body).to have_no_contentJSON objects, JSON arrays, and whitespace-only bodies are considered content.
schema = { status: "ready", count: 2 }Classes use instance_of?, so subclasses do not match:
schema = { id: Integer, name: String }A regular expression matches only a JSON String. Numbers, booleans, and null do not match after conversion:
schema = { color: /\A#[0-9a-fA-F]{6}\z/ }Use \A and \z for whole-string validation. Ruby's ^ and $ are line anchors and may accept a matching line inside a multiline value.
Array schemas have three forms:
[String] # any-length list of Strings
[{ id: Integer, name: String }] # any-length list of this object shape
[Integer, String] # an exact two-element tupleThe one-element shorthand applies only to a Class or Hash. For example, [Types::UUID] means an exact one-element array because Types::UUID is a Regexp.
A constraint Proc takes no arguments and returns an options Hash:
schema = {
age: -> { { type: Integer, min: 18, max: 120 } },
role: -> { { inclusion: %w[admin member] } },
code: -> { { regex: /\A[A-Z]{3}\z/ } },
even: -> { { lambda: ->(value) { value.even? } } },
nickname: -> { { type: String, allow_blank: true } }
}Supported options are allow_blank, type, value, min, max, inclusion, regex, and lambda. All supplied constraints must pass. Unknown options, a non-Hash return value, or a Proc that declares an argument raises ArgumentError with usage guidance.
allow_blank: true accepts null, false, empty strings, whitespace-only strings, empty arrays, and empty objects. The key itself remains required.
The built-in types are anchored regular expressions:
RSpec::JsonApi::Types::EMAIL
RSpec::JsonApi::Types::URI
RSpec::JsonApi::Types::UUIDURI accepts schemes supported by Ruby's standard URI parser, not only HTTP and HTTPS.
Generate a custom type with Rails:
rails generate rspec:json_api:type color_hexOr define one directly:
module RSpec
module JsonApi
module Types
COLOR_HEX = /\A#(?:[0-9a-fA-F]{3}){1,2}\z/
end
end
endInterfaces are reusable strict object schemas:
module RSpec
module JsonApi
module Interfaces
PERSON = {
id: Types::UUID,
name: String,
active: -> { { inclusion: [true, false] } }
}.freeze
end
end
endGenerate one with:
rails generate rspec:json_api:interface personUse an interface directly or as a homogeneous list schema:
expect(response.body).to match_json_schema(RSpec::JsonApi::Interfaces::PERSON)
expect(response.body).to match_json_schema([RSpec::JsonApi::Interfaces::PERSON])Use the Ruby version in .ruby-version and Bundler 4.0.4:
gem install bundler -v 4.0.4
bundle install
bundle exec rspec
bundle exec rubocop
bundle exec bundle-audit check --updateSee CONTRIBUTING.md for compatibility and contribution guidance. Please report vulnerabilities using GitHub's private security advisory form, as described in SECURITY.md.
The gem is available under the terms of the MIT License.
Everyone participating in this project is expected to follow the code of conduct.