Keep your sanity and use JSON Schema to validate nested JSON documents

Let's take a hypothetical JSON object that holds various pieces of data and describes a request for currency conversion:

{
  "id": "MdfDoPWq",
  "amount": 1000,
  "from": "EUR",
  "to": "USD",
  "notify": [
    {
      "name": "John",
      "email": "john@gmail.com"
    },
    {
      "name": "Jack",
      "email": "jack@gmail.com"
    }
  ]
}

Just by looking at the JSON document it's clear that there are a few rules it should follow to be correctly interpreted and processed.

Let's write these rules down:

  • The ID should be a 10 letter string
  • The amount should be an integer between 0 - 10000
  • The from/to currency can be one of: EUR, USD, GBP
  • The notify array of objects should contain name/email pairs
  • The id, amount, from, to properties should be required
  • The notify array should be optional

Ensuring that a JSON document is valid (especially when it's deeply nested) can be a challenge. Luckily, JSON Schema comes to the rescue and can help with validating the JSON document's structure in whatever way is necessary.

Now let's write a JSON Schema that defines all the rules the currency conversion object should follow to be valid. You'll immediately notice a few useful features of JSON Schema such as property type validation, array validation, reusable definitions, etc. There are many more features that you may find equally useful. For a complete specification and a detailed guide visit http://json-schema.org/.

{
  "$schema": "http://json-schema.org/schema#",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[A-Za-z]{10}$"
    },
    "amount": {
      "type": "integer",
      "minimum": 0,
      "maximum": 10000
    },
    "from": {
      "$ref": "#/definitions/currency"
    },
    "to": {
      "$ref": "#/definitions/currency"
    },
    "notify": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "format": "email"
          }
        }
      }
    }
  },
  "required": [
    "id",
    "amount",
    "from",
    "to"
  ],
  "additionalProperties": false,
  "definitions": {
    "currency": {
      "type": "string",
      "pattern": "^(EUR|USD|GBP)$"
    }
  }
}

There are a number of libraries that help with validation using JSON Schema. If you're working with Laravel you should definitely take a look at the JSON Guard package.

I wrapped the JSON Schema in Laravel's validation rule class.

namespace App\Validation;

use Illuminate\Contracts\Validation\Rule;
use League\JsonGuard\Validator;

class CurrencyConversionValidator implements Rule
{
    public function passes($attribute, $value)
    {
        return (new Validator(json_decode($value), $this->schema()))->passes();
    }

    private function schema()
    {
        return json_decode('{
            "$schema": "http://json-schema.org/schema#",
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{10}$"
                },
                "amount": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000
                },

                ...
        }');
    }

    public function message()
    {
        return trans('validation.currency_conversion');
    }
}

To make use of the validator it's as easy as:

use App\Validation\CurrencyConversionValidator;
use Illuminate\Foundation\Http\FormRequest;

class CurrencyConversionRequest extends FormRequest
{
    public function rules()
    {
        return [
            'json' => ['required', 'json', new CurrencyConversionValidator($this->json)],
        ];
    }

    ...
}
Did you like this post?
Previous post

I was looking for a way to handle single click and double click events on the same element in Vue.js. At first it looked like 'v-on:dblclick' is going to cover this requirement nicely, but I soon discovered it's not ideal. While double clicking the 'click' event fires too (as soon as the first click of an intended double click is registered), which makes it hard to handle both types of events at the same time.

Next post

Portals provide a way to render content into a DOM node that exists outside the hierarchy of the Vue component that creates the portal. The concept of portals also exists in React. When the need arises I rely on the portal-vue library to create portals, which provides a really clean syntax.