Client-side json:api resource validation with ajv

3 minute read

Part of signing up for Meet Kinksters involves ensuring users provide sufficient (and valid) information about themselves and their dating preferences. This data is of course validated on the server, but I needed a way to provide UI feedback regarding incomplete Drupal entities (surfaced as json:api resources) in real time.

The problem

I discussed this topic on the Drupal Slack #contenta (API-first) channel, and my open-source colleagues suggested a number of possible solutions. Most suggested posting the entire entity and watching a returned meta object for details on validation errors. By default, Drupal’s json:api implementation validates required fields and all field data and returns a 422 Unprocessable Entity error on failure. I could change this behavior, as the Commerce API module does, but this seemed a little too bespoke for my needs. In addition, I wanted to maintain the ability to PATCH specific entity fields as the user completes their profile, saving bandwidth and making operations more atomic.

json-schema to the rescue

I was already using Drupal’s jsonapi_schema module to retrieve enumerations of field options, and so I wondered if these JSON Schema definitions could also validate client-side copies of entities under construction. It turns out, they can!

I did need to make a few bugfixes to the Drupal module while I was at it, but eventually I was able to feed the relevant schemas into ajv, an awesome JavaScript JSON schema validator. First the code, and then some gotchas.

import Ajv2019 from "ajv/dist/2019"
import hyperSchema from "../lib/hyper-schema.json";
import hyperSchemaMeta from "../lib/hyper-schema-meta.json";
import linksSchema from "../lib/links-schema.json";
import jsonApiSchema from "../lib/jsonapi-schema.json";
import addFormats from "ajv-formats"

const ajv = new Ajv2019({
  strict: false,
  // Validating formats can be a security risk in production, and slow.
  validateFormats: __DEV__,
  // One error might be enough to block progress.
  allErrors: __DEV__,
});
__DEV__ && addFormats(ajv);
// Required b/c json:api uses it.
const draft7MetaSchema = require("ajv/dist/refs/json-schema-draft-07.json")
ajv.addMetaSchema(draft7MetaSchema)
ajv.addMetaSchema(hyperSchemaMeta, undefined, false);
ajv.addSchema(linksSchema, undefined, true);
ajv.addSchema(hyperSchema, undefined, true);
ajv.addSchema(jsonApiSchema, 'https://jsonapi.org/schema', true);

// ... Schemas are retrieved from Drupal.

// Inside your function component:
useEffect(() => {
  const userValid = userSchemaValidator(context.user);
  // Do something with the result.
  setSchemasValidate(userValid);
  __DEV__ && console.log(
    ['User Validation Errors', userSchemaValidator.errors, context.user, profileSchemaValidator.schema]
  );
}, [context.user]);

What to do with the validation results are up to you, of course.

Schema validation is hard

It took a whole day’s work to get to a working state. The client side of the equation was the simplest part; ajv is well-documented and supported. The difficulty came in fixing a number of issues in my dependencies:

  • json:api schema generation in Drupal is still very much under development. Both the jsonapi_schema and jsonapi_extras modules needed significant work to generate truly valid schema. I have opened an issue against the schemata project summarizing the current issues. If you’re working in this space and can contribute, we’d welcome your input. The main sticking points within Drupal’s implementation had to do with handling nullability for non-required fields.
  • The JSON schema for json:api itself was incorrect/out of date. There is a PR open to update it, however I ran into some issues with validation when paired with Drupal’s jsonapi_hypermedia module. Thankfully, since I am vendoring the proposed schema file within my project, I could update it without much trouble.
  • It’s not at all intuitive to validate JSON Hyper-Schema with ajv; I had to vendor a number of manually-downloaded schema and meta-schema to include in the validator compilation (as you can see above.) I don’t necessarily think ajv needs to include this schema in its distribution, but it did catch me by surprise.

Was it worth the trouble?

I spent a whole day wrestling the various components in play, mostly within Drupal. I did learn a lot, for instance the use-case for JSON Hyper-Schema in describing json:api documents. My original goal was to roll up the validation status of various entities under construction on the client side, and I still don’t see any real way to do this within my chosen stack. Hopefully the issues I’ve uncovered regarding Drupal’s schema generation will help others get up and running faster. I certainly couldn’t be building such a complex application without the excellent prior efforts of other open-source maintainers.

Updated: