---
title: Validating JSON Schemas with an RSpec Matcher
teaser: 'Use RSpec and JSON Schema to create a test-driven process in which changes
  to the structure of your JSON API drive the implementation of new features.

  '
tags: web,json,ruby,rspec,testing
author: Laila Winner
published_on: 2014-09-23
---

At thoughtbot we've been experimenting with using [JSON
Schema](http://json-schema.org), a widely-used specification for describing the
structure of <abbr title="JavaScript Object Notation">JSON</abbr> objects, to
improve workflows for documenting and validating <abbr title="JavaScript Object
Notation">JSON</abbr> <abbr title="Application Programming
Interface">API</abbr>s.

Describing our <abbr title="JavaScript Object Notation">JSON</abbr> <abbr
title="Application Programming Interface">API</abbr>s using the <abbr
title="JavaScript Object Notation">JSON</abbr> Schema standard allows us to
automatically generate and update our HTTP clients using tools such as
[heroics](https://github.com/interagent/heroics) for Ruby and
[Schematic](https://github.com/interagent/schematic) for Go, saving loads of
time for client developers who are depending on the API. It also allows us to
improve test-driven development of our API.

If you've worked on a test-driven <abbr title="JavaScript Object
Notation">JSON</abbr> <abbr title="Application Programming Interface">API</abbr>
written in Ruby before, you've probably encountered a request spec that looks
like this:

```ruby
describe "Fetching the current user" do
  context "with valid auth token" do
    it "returns the current user" do
      user = create(:user)
      auth_header = { "Auth-Token" => user.auth_token }

      get v1_current_user_url, {}, auth_header

      current_user = response_body["user"]
      expect(response.status).to eq 200
      expect(current_user["auth_token"]).to eq user.auth_token
      expect(current_user["email"]).to eq user.email
      expect(current_user["first_name"]).to eq user.first_name
      expect(current_user["last_name"]).to eq user.last_name
      expect(current_user["id"]).to eq user.id
      expect(current_user["phone_number"]).to eq user.phone_number
    end
  end

  def response_body
    JSON.parse(response.body)
  end
end
```

Following the [four-phase test pattern], the test above executes a request to
the current user endpoint and makes some assertions about the structure and
content of the expected response. While this approach has the benefit of
ensuring the response object includes the expected values for the specified
properties, it is also verbose and cumbersome to maintain.

[four-phase test pattern]: https://thoughtbot.com/blog/four-phase-test

Wouldn't it be nice if the test could look more like this?

```ruby
describe "Fetching the current user" do
  context "with valid auth token" do
    it "returns the current user" do
      user = create(:user)
      auth_header = { "Auth-Token" => user.auth_token }

      get v1_current_user_url, {}, auth_header

      expect(response.status).to eq 200
      expect(response).to match_response_schema("user")
    end
  end
end
```

Well, with a dash of RSpec and a pinch of <abbr title="JavaScript Object
Notation">JSON</abbr> Schema, it can!

## Leveraging the flexibility of RSpec and JSON Schema

An important feature of <abbr title="JavaScript Object Notation">JSON</abbr>
Schema is [instance
validation](http://json-schema.org/latest/json-schema-validation.html). Given a
<abbr title="JavaScript Object Notation">JSON</abbr> object, we want to be able
to validate that its structure meets our requirements as defined in the schema.
As providers of an HTTP <abbr title="JavaScript Object Notation">JSON</abbr>
API, our most important <abbr title="JavaScript Object Notation">JSON</abbr>
instances are in the response body of our HTTP requests.

[RSpec provides a DSL](https://www.relishapp.com/rspec/rspec-expectations/v/3-1/docs/custom-matchers/define-matcher)
for defining custom spec matchers. The [json-schema](https://github.com/hoxworth/json-schema)
gem's *raison d'être* is to provide Ruby with an interface for validating <abbr
title="JavaScript Object Notation">JSON</abbr> objects against a <abbr title="JavaScript
Object Notation">JSON</abbr> schema.

Together these tools can be used to create a test-driven process in which
changes to the structure of your <abbr title="JavaScript Object
Notation">JSON</abbr> <abbr title="Application Programming Interface">API</abbr>
drive the implementation of new features.

## Creating the custom matcher

First we'll add [json-schema](https://github.com/hoxworth/json-schema) to our
`Gemfile`:

```ruby
group :test do
  gem "json-schema"
end
```

Next, we'll define a custom RSpec matcher that validates the response object in
our request spec against a specified <abbr title="JavaScript Object
Notation">JSON</abbr> schema:

In `spec/support/api\_schema\_matcher.rb`:

```ruby
RSpec::Matchers.define :match_response_schema do |schema|
  match do |response|
    schema_directory = "#{Dir.pwd}/spec/support/api/schemas"
    schema_path = "#{schema_directory}/#{schema}.json"
    JSON::Validator.validate!(schema_path, response.body, strict: true)
  end
end
```

We're making a handful of decisions here: We're designating
`spec/support/api/schemas` as the directory for our <abbr title="JavaScript
Object Notation">JSON</abbr> schemas and we're also implementing a naming
convention for our schema files.

`JSON::Validator#validate!` [is provided by the json-schema
gem](https://github.com/hoxworth/json-schema#usage). Passing `strict: true` to
the validator ensures that validation will fail when an object contains
properties not defined in the schema.

## Defining the user schema

Finally, we define the `user` schema using the <abbr title="JavaScript Object
Notation">JSON</abbr> Schema specification:

In `spec/support/api/schemas/user.json`:

```json
{
  "type": "object",
  "required": ["user"],
  "properties": {
    "user" : {
      "type" : "object",
      "required" : [
        "auth_token",
        "email",
        "first_name",
        "id",
        "last_name",
        "phone_number"
      ],
      "properties" : {
        "auth_token" : { "type" : "string" },
        "created_at" : { "type" : "string", "format": "date-time" },
        "email" : { "type" : "string" },
        "first_name" : { "type" : "string" },
        "id" : { "type" : "integer" },
        "last_name" : { "type" : "string" },
        "phone_number" : { "type" : "string" },
        "updated_at" : { "type" : "string", "format": "date-time" }
      }
    }
  }
}
```

## TDD, now with schema validation

Let's say we need to add a new property, `neighborhood_id`, to the `user`
response object. The back end for our <abbr title="JavaScript Object
Notation">JSON</abbr> API is a Rails application using
[ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers).

We start by adding `neighborhood_id` to the list of required properties in the
user schema:

In `spec/support/api/schemas/user.json`:

```json
{
  "type": "object",
  "required": ["user"],
  "properties":
    "user" : {
      "type" : "object",
      "required" : [
        "auth_token",
        "created_at",
        "email",
        "first_name",
        "id",
        "last_name",
        "neighborhood_id",
        "phone_number",
        "updated_at"
      ],
      "properties" : {
        "auth_token" : { "type" : "string" },
        "created_at" : { "type" : "string", "format": "date-time" },
        "email" : { "type" : "string" },
        "first_name" : { "type" : "string" },
        "id" : { "type" : "integer" },
        "last_name" : { "type" : "string" },
        "neighborhood_id": { "type": "integer" },
        "phone_number" : { "type" : "string" },
        "updated_at" : { "type" : "string", "format": "date-time" }
      }
    }
  }
}
```

Then we run our request spec to confirm that it fails as expected:

    Failures:

      1) Fetching a user with valid auth token returns requested user
         Failure/Error: expect(response).to match_response_schema("user")
         JSON::Schema::ValidationError:
           The property '#/user' did not contain a required property of 'neighborhood_id' in schema
           file:///Users/laila/Source/thoughtbot/json-api/spec/support/api/schemas/user.json#

    Finished in 0.34306 seconds (files took 3.09 seconds to load)
    1 example, 1 failure

    Failed examples:

    rspec ./spec/requests/api/v1/users_spec.rb:6 # Fetching a user with valid auth token returns requested user

We make the test pass by adding a `neighborhood_id` attribute in our serializer:

```ruby
class Api::V1::UserSerializer < ActiveModel::Serializer
  attributes(
    :auth_token,
    :created_at,
    :email,
    :first_name,
    :id,
    :last_name,
    :neighborhood_id,
    :phone_number,
    :updated_at
  )
end
```

    .

    Finished in 0.34071 seconds (files took 3.14 seconds to load)
    1 example, 0 failures

    Top 1 slowest examples (0.29838 seconds, 87.6% of total time):
      Fetching a user with valid auth token returns requested user
        0.29838 seconds ./spec/requests/api/v1/users_spec.rb:6

Hooray!

## What's next

* Read more about <abbr title="JavaScript Object Notation">JSON</abbr> Schema in
  [this overview by Brandur Leach](http://brandur.org/elegant-apis).
* Get inspired by the [Space Telescope Science Institute's
  guide](http://spacetelescope.github.io/understanding-json-schema/) to
  structuring schemas using <abbr title="JavaScript Object Notation">JSON</abbr>
  Schema.
