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 quite likely a few rules it should follow to be correctly interpreted and processed.

Let's specify and 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 surely 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 above JSON Schema in Laravel's validation rule class, so that it can be used as part of request validation:

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)],
        ];
    }

    ...
}