Testing your JSON API in Ruby with dry-rb
Tags: ruby, json, api, testing
Published: 2022-04-29 12:21:42

---
title: Testing your JSON API in Ruby with dry-rb
published: true
description: How to test JSON API responses with Ruby in a maintainable and readable way by using dry-rb gems, such as dry-schema & dry-validation
tags: Ruby, JSON, API, testing
canonical_url: https://medium.com/@paulgoetze/testing-your-json-api-in-ruby-with-dry-rb-dffb6a9bccdf
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh7v2h7sq2osmot0jp47.png
---

When writing a JSON API with Ruby in your favorite web framework, you will certainly come to a point, where you have to decide how to test your endpoints. In this article I’m going to show one of my preferred ways to test the structure of JSON responses in a clear and readable way—by making use of the dry-schema gem and some other dry-rb libraries.

---

## There’s Different Ways

When it comes to testing your JSON API endpoints in Ruby, you can go for plenty of different approaches. The simplest probably is to check the status code and explicitly assert each value of you response data.

Let’s assume we want to test a `GET /todos` endpoint, which returns to-do items like this:

{% embed https://gist.github.com/paulgoetze/f6e1753b4892c127c6776259c70d81c5 %}


Then a simple test for this response could look like this:

{% embed https://gist.github.com/paulgoetze/001ecc6fa78515a1a08078a540b82cbb %}


However, this approach is a bit cumbersome for large response bodies and can result in a lot of accesses to nested hash keys, until you get to the value you actually want to check. It will also take you quite some effort to adjust the tests as soon as some keys or values in the JSON response change.

So, instead of checking for each exact value, you could move up one level of abstraction and only check for the right structure and data types in your response body. Again there’s different ways to do that.

You could use JSON Schema files and validate your response against them, as is described here:

{% embed https://thoughtbot.com/blog/validating-json-schemas-with-an-rspec-matcher %}

The JSON Schema approach works quite well. However, writing JSON Schema files can get a bit tricky and confusing, especially when you have to deal with complex and nested JSON data. There’s ways to handle this complexity, e.g. by splitting up and [reusing schemas by referencing](https://medium.com/grammofy/handling-complex-json-schemas-in-python-9eacc04a60cf?sk=2acab57dc699857181742e153f3e58bb) them.

But maybe there’s a more legible and maintainable way than JSON Schema validation. Let’s explore the realm of dry-rb for this purpose.

## What is dry-rb?

[dry-rb](https://dry-rb.org/) is a collection of Ruby libraries, whose goal is to encapsulate common tasks–like data validation, data transformation, Ruby object initialization, and others–in small, reusable, and framework-independent Ruby gems.

For our purpose of validating the structure and types of JSON data it provides us with the [dry-validation](https://dry-rb.org/gems/dry-validation), [dry-schema](https://dry-rb.org/gems/dry-schema), and [dry-types](https://dry-rb.org/gems/dry-types) gems. These gems give us all we need for setting up a schema to validate JSON data of almost any complexity.

So let’s have a look at some validation use cases and let’s see how we can leverage the mentioned dry-rb gems for a simple JSON structure. We will also dig into a more complex one afterwards.

## A Simple JSON Response

The [dry-schema](https://dry-rb.org/gems/dry-schema) gem comes with a `Dry::Schema` module which has a `Params` method to define schemas for validating hashes. Built on top, it also has a `JSON` method for defining JSON schemas with coercible data types, like `Date` or `Time`. This is the one we want to use for our response validation.

A schema for one of our to-do items from above would look something like this:

{% embed https://gist.github.com/paulgoetze/f87225e4cc2d9b9a1a7f421a2534d6c2 %}

With these definitions we make sure that an `id`, `title`, and the `done` flag is present in our to-do item JSON object–and that each property has the given type.

Instead of `required()` we can also use `optional()`, to allow the object to not have this property. Instead of `value()` we can also put `maybe()` to allow nil values.

Having this schema defined, we can now run the validation in our test for each to-do item by calling the schema, instead of checking for each value explicitly as we did before:

{% embed https://gist.github.com/paulgoetze/ae36b52454a2dbd6acc3fa2b5c7772ff %}

Dry-schema also allows us to nest schemas and to pass schemas as property types. So, let’s build a schema for our entire JSON response:

{% embed https://gist.github.com/paulgoetze/214773182afbf6aec14709da79048bbf %}

Putting all of this together in our test case we can now validate our response body to match the `ResponseSchema`:

{% embed https://gist.github.com/paulgoetze/a30f11e0e4aa1bef2ba944f56e53494c %}

Of course you might still want to check some of the JSON data’s properties explicitly, e.g. to make sure the right to-do ids are involved.

Nevertheless, using dry-schemas to validate the response structure makes your test code a bit more concise already and also makes it easier to understand what the actual JSON response looks like. You can have a quick look at the involved schemas to get an idea about required or optional properties and their (maybe nullable) types.


## A More Complex JSON Response

You might think: “Well, nice little examples, but my real-world JSON data is way more complex. Can I use this for these structures, too?”.

I think you can. Let’s see how.

Let’s replace some parts of our simple to-do JSON with more complex ones. Instead of the `done` flag we will have a `done_at` timestamp, we will add a `created_at` timestamp and a `priority` (which could be one of `high`, `default`, or `low`). We will also assume that we have a collaborative to-do list, where we want to include the creator and assignee for each to-do item. Last, let’s have an arbitrary list of tags that can be assigned for a to-do item. This is how our new JSON response might look like:

{% embed https://gist.github.com/paulgoetze/3df5b0a82fe1586b7a52fb5a990274d4 %}

A very basic schema for this response would be something like the following:

{% embed https://gist.github.com/paulgoetze/ff8d932dcfc91a83e30c85e3c2cf65c7 %}

Note that we used `maybe()` for the `done_at` property, which allows to have nil values.

This only validates the presence and the top level type of each property. The [dry-types](https://dry-rb.org/gems/dry-types) gem that is used by our schema, however, provides us with a number of built-in types, like `Types::JSON::DateTime` (which will be used under the hood if we type `:date_time`), `Types::Array`, and `Types::String.enum`. We can use these to refine our timestamps and tags types in the schema. In order to create custom types we need to create a `Types` module and include the dry types. Similar to what we already saw for the simple JSON response, we can also create and reuse sub-schemas for the `creator` and `assignee` properties:

{% embed https://gist.github.com/paulgoetze/f63dbc06d8db722ac6a437f88d394b10 %}

This now looks already more like a real-world use case!

Still, we can go one step further and check for the integrity of certain properties. With [dry-validation](https://dry-rb.org/gems/dry-validation) (which is powered by dry-schema) we have a tool at hand, that allows us to define custom rules for our JSON data on top of our existing schema definitions.

Before we can define custom rules we need to change our schema definition from the `Dry::Schema.JSON` block to a response class that inherits from the `Dry::Validation::Contract` class:

{% embed https://gist.github.com/paulgoetze/3c80aa396a3c8533076fe5db610d0e68 %}

The `Dry::Validation::Contract` base class comes with a `rule` method that we can use to raise custom validation failures if certain conditions match.

Inside a rule we have access to a `values` variable, which holds the value(s) of the configured property for that rule. As soon as we assign a failure message (by calling `key.failure("some message"))` we will make the validation fail.

Let’s say we want to validate the `done_at` DateTime to always be after the `created_at` DateTime, and each tag in `tags` to only appear once. Then these might be our rule definitions:

{% embed https://gist.github.com/paulgoetze/7129ace7fc8d427c03f5d23e197f53ed %}

You can define whatever rules you can come up with to customize your validation contract and to adjust it to your needs. Dry-rb already provides the most often used data types, but you can also use fully customized ones as we saw in the examples above.

Note that our first `ResponseSchema` definition, where we made use of `Dry::Schema.JSON`, gave us an instance of the schema class already. When using the `Dry::Validation::Contract` we need to create an instance of our `ResponseSchema` ourselves, before we can call its `call()` method to validate our data.

For more details and dry-validation features I encourage you to checkout the [dry-validation documentation](https://dry-rb.org/gems/dry-validation).


## Some Assertion Syntactical Sugar 🍬

One issue with directly asserting if a schema validation succeeds is that you won’t see what went wrong if the validation failed. Dry-schema provides detailed error messages, so we can make use of these to debug our failing response validation.

We can fix the visibility issue of failure messages by setting up a small test helper method:

{% embed https://gist.github.com/paulgoetze/f634bab066b229173a944e4b210f54c3 %}

If the validation fails you will now be pointed to the erroneous properties and you will see a message about what went wrong. On top you will get the original JSON response, so you can spot the validation issue in no time.


## Conclusion

We saw that dry-rb gems, specifically the [dry-schema](https://dry-rb.org/gems/dry-schema) and [dry-validation](https://dry-rb.org/gems/dry-validation) gems provide us with some useful tools to make JSON response tests in Ruby more legible and maintainable.

On top of making our JSON response tests more concise, the schemas we set up with this approach also allow us to have a quick overview about the structure and data types of our endpoint’s response.

Schemas can be reused in other schemas and tests. They are easy to change and framework agnostic. So, although I used minitest assertion syntax in my code examples, with some small adjustments the shown approaches can also be applied if you use RSpec or any other Ruby testing library.

---

_I hope you enjoyed this little journey into JSON response testing with dry-rb—thanks for reading and I’m curious to hear about your experiences with using dry-rb gems or similar approaches in your testing environment._

_Happy coding, happy testing!_

---

<small>Credits: dryer icon in the header image by [photo3idea_studio](https://www.flaticon.com/authors/photo3idea-studio) from [flaticon.com](https://www.flaticon.com/) </small>