Validating the FormKeep API

Sean Doyle

FormKeep’s administrative interface is an Ember application that consumes a Rails-backed JSON API. We test our JSON endpoints with RSpec’s request specs.

Ensuring that the client and server adhere to a common contract is important. Without test coverage, the contract is implicit and often times brittle. Implementation changes on the server that change JSON payloads in unexpected ways pose problems for consumers.

To guard against unexpected changes, we created json_matchers to validate the format of our JSON endpoints, making our contract explicit. Under the covers, json_matchers validates payloads against schemas conforming to the JSON Schema specification. Schemas serve as a codification of the client-server contract.

Asserting responses adhere to a JSON Schema

In order to verify that GET /api/forms serves JSON in the format our client expects, we’ll assert that it successfully validates against a particular JSON schema:

describe "GET /api/forms" do
  it "returns the user's forms" do
    user = create(:user)
    form = create(:form, user: user)

    create(:form)

    json_get "/api/forms", api_token: user.api_token

    expect(response).to be_successful
    expect(response).to match_response_schema("forms-many")
  end
end

def json_response
  JSON.parse(response.body)
end

Next, we’ll create the forms-many schema, declared in spec/support/api/schemas/forms-many.json:

{
  "type": "object",
  "required": [
    "forms"
  ],
  "properties": {
    "forms": {
      "type": "array",
      "items": { "$ref": "form.json" }
    }
  }
}

Note the "$ref": "form.json", key-value pair. It denotes a canonical dereferencing, which allows us to reference and reuse a form schema that we declare separately in spec/support/api/schemas/form.json:

{
  "type": "object",
  "required": [
    "id",
    "name",
    "redirect_url",
    "webhook_url",
    "field_map",
    "sandboxed"
  ],
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "redirect_url": { "type": "string" },
    "webhook_url": { "type": "string" },
    "field_map": { "type": ["object", "null"] },
    "sandboxed": { "type": "boolean" }
  }
}

These two schemas, combined with the match_response_schema matcher, guarantee the format, structure, and value types of the API response. Schema changes necessitate serializer code changes, and vice versa.

This is a good thing, as it helps guarantee that your client’s expectations match the server’s behavior.

Next steps