Quick start

Some quick examples about how to use Opis JSON Schema

To better exemplify the benefits of using JSON Schema for validating JSON documents, were gonna build a validation schema for a set of data representing the profile of some web application’s user. For the sake of simplicity we don’t care how we obtained these data, and we will simply assume that they are stored in a variable called $data.

Here is a possible content for this variable:

{
    "name": "John Doe",
    "age": 31,
    "email": "john@example.com",
    "website": null,
    "location": {
        "country": "US",
        "address": "Sesame Street, no. 5"
    },
    "available_for_hire": true,
    "interests": ["php", "html", "css", "javascript", "programming", "web design"],
    "skills": [
        {
            "name": "HTML",
            "value": 100
        },
        {
            "name": "PHP",
            "value": 55
        },
        {
            "name": "CSS",
            "value": 99.5
        },
        {
            "name": "JavaScript",
            "value": 75
        }
    ]
}

The validation schema

Now that we have an idea over how our data are structured, its time to start creating a validation schema. The JSON contained in $data is structured as an object with multiple properties. All the properties are required to be present and adding additional properties is forbidden. The validation schema that follows these rules, looks something like bellow.

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "http://api.example.com/profile.json",
    "type": "object",
    "properties": {
    },
    "required": ["name", "age", "email", "location", 
                 "available_for_hire", "interests", "skills"],
    "additionalProperties": false
}

Neither the $schema, nor the $id keywords are required in order for our validation schema to work. We only added them as an example of good practice.

Object’s properties

The only thing that is missing from the above validation schema are the validation rules for its properties. Let’s take each of the object’s property in turn, explain it, and write some validation rules for them.

name

The name property must be a non-empty string that contains at most 64 characters. We also added a pattern constraint, to make sure that the value of the name property follows a specific format.

{
    "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64,
        "pattern": "^[a-zA-Z0-9\\-]+(\\s[a-zA-Z0-9\\-]+)*$"
    }
}
age

The age property is an integer between 18 and 100.

{
    "age": {
        "type": "integer",
        "minimum": 18,
        "maximum": 100
    }
}
email

This is a string that must be formatted as an email and its maximum length is of 128 characters.

{
    "email": {
        "type": "string",
        "maxLength": 128,
        "format": "email"
    }
}
website

If the user doesn’t have a website, then the value of this property must be null. Otherwise, this is a string of a maximum length of 128 characters, formatted as a hostname.

{
    "website": {
        "type": ["string", "null"],
        "maxLength": 128,
        "format": "hostname"
    }
}
location

This property is an object that contains two other properties: country and address. The country property is a two-letter string representing the country’s code. To keep things simple we only support United State, Canada and United Kingdom. Therefore we could just use an enum. The address property is just a string that can contains at most 128 characters. Both these properties are required, and the object doesn’t support additional properties.

{
    "location": {
        "type": "object",
        "properties": {
             "country": {
                 "enum": ["US", "CA", "GB"]
             },
             "address": {
                 "type": "string",
                 "maxLength": 128
             }
        },
        "required": ["country", "address"],
        "additionalProperties": false
    }
}
available_for_hire

The property’s value must be a boolean

{
    "available_for_hire": {
        "type": "boolean"
    }
}
interests

This must be an array of strings. It must contain at least 3 items and a maximum of 100. Each string within the array must have a maximum length of 64 characters and must be unique to the array.

{
    "interests": {
        "type": "array",
        "minItems": 3,
        "maxItems": 100,
        "uniqueItems": true,
        "items": {
            "type": "string",
            "maxLength": 64
        }
    }
}
skills

This is also an array, but this one contains objects. Each object has two required properties: name and value. The name properties represents the name of the skill that user have, and it’s a non-empty string of a maximum length of 64 characters. The value property tells how skilled is the user using a float number between 0 and 100. That number can be expressed as multiples of 0.25. Each object within the array must be unique and it doesn’t accept new propeties. There isn’t a minimum number of skills that a user can have, so the array can be empty, but there is a maximum of 100 skills that can be declared.

{
    "skills": {
        "type": "array",
        "maxItems": 100,
        "uniqueItems": true,
        "items": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "minLenght": 1,
                    "maxLength": 64
                },
                "value": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "multipleOf": 0.25
                }
            },
            "required": ["name", "value"],
            "additionalProperties": false
        }
    }
}

Putting all together

Now that we have finished defining validation rules, let’s put all together and see the resulting validation schema.

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "http://api.example.com/profile.json",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "pattern": "^[a-zA-Z0-9\\-]+(\\s[a-zA-Z0-9\\-]+)*$"
        },
        "age": {
            "type": "integer",
            "minimum": 18,
            "maximum": 100
        },
        "email": {
            "type": "string",
            "maxLength": 128,
            "format": "email"
        },
        "website": {
            "type": ["string", "null"],
            "maxLength": 128,
            "format": "hostname"
        },
        "location": {
            "type": "object",
            "properties": {
                 "country": {
                     "enum": ["US", "CA", "GB"]
                 },
                 "address": {
                     "type": "string",
                     "maxLength": 128
                 }
            },
            "required": ["country", "address"],
            "additionalProperties": false
        },
        "available_for_hire": {
            "type": "boolean"
        },
        "interests": {
            "type": "array",
            "minItems": 3,
            "maxItems": 100,
            "uniqueItems": true,
            "items": {
                "type": "string",
                "maxLength": 64
            }
        },
        "skills": {
            "type": "array",
            "maxItems": 100,
            "uniqueItems": true,
            "items": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "minLenght": 1,
                        "maxLength": 64
                    },
                    "value": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "multipleOf": 0.25
                    }
                },
                "required": ["name", "value"],
                "additionalProperties": false
            }
        }
    },
    "required": ["name", "age", "email", "location", 
                 "available_for_hire", "interests", "skills"],
    "additionalProperties": false
}

The PHP part

Believe it or not, the PHP part is the most trivial part of the validation process. Once we have the validation schema, we can save it into a file like schema.json and start validating the content of $data variable.

use Opis\JsonSchema\{
    Validator,
    ValidationResult,
    Errors\ErrorFormatter,
};

// Create a new validator
$validator = new Validator();

// Register our schema
$validator->resolver()->registerFile(
    'http://api.example.com/profile.json', 
    '/path/to/schema.json'
);

$data = <<<'JSON'
{
    "name": "John Doe",
    "age": 15,
    ... rest of the properties
}
JSON;

// Decode $data
$data = json_decode($data);

/** @var ValidationResult $result */
$result = $validator->validate($data, 'http://api.example.com/profile.json');

if ($result->isValid()) {
    echo "Valid", PHP_EOL;
} else {
    // Print errors
    print_r((new ErrorFormatter())->format($result->error()));
}