How to apply JSON Schema in game development

Verónica Valls
Game & Frontend Development Stuff
7 min readSep 4, 2018
JSON Schema X games

What is JSON Schema

According to http://json-schema.org/ :

JSON Schema is a vocabulary that allows you to annotate and validate JSON documents.

Advantages

  • Describes your existing data format(s).
  • Provides clear human- and machine- readable documentation.

Validates data which is useful for:

  • Automated testing.
  • Ensuring quality of client submitted data.

How does JSON Schema fit in game development

If you provide any data through JSON to your game, you can apply JSON Schema on them. Sceneries, items, characters customization, assets, game levels parameters… You can define a schema for any object definition you need or that you’re currently using in your game.

The advantages are:

  • The JSON’s structure is checked: If a JSON file is created manually or automatically by a script, you can detect errors coming from the script generator or typos from the person who edited it.
  • The data structure is defined: Every person working on the project can check the schemas related and know what is the data needed and how it is structured, no more misleads.
  • Control on data parameters and parameters format: You can define that an object follows compulsory the schema definitions you’ve defined.
    For instance, imagine we have a game title text definition, this text only needs a font family, size and color. We can define that no additional properties are added to this case, otherwise JSON Schema will alert an error.
    Another example, if we need that size property has to be defined as an integer, it can also be required by the schema definition, so, if the incoming value is a string, an error will be displayed.

JSON Schema use case: Layout Previewer

I used JSON Schema in a project I developed where designers can preview the graphics and layout of the next game without needing the real game project to be ready and working to that point.

On this case, the purpose of JSON Schema is to define the type of objects that the layout can contain, such as sprites, buttons, texts and layout containers, and its limitations, if any limitation is crossed, the tool is prepared to show logs of the errors and warnings on Chrome Developers Tools Console.

Defining the schemaToLoad.json

{
"$async": true,
"$schema": "http://json-schema.org/draft-07/schema#",
"$id":"schemaToLoad.json",
"type":"object",
"title": "Layout",
"description": "Layout elements that compose a view: Sprites, buttons, texts and layouts",
"patternProperties": {
"^(?=.*Sprite)(?!.*Button)(?!.*Text)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "schemaDefinitionsToLoad.json#/definitions/sprite" },
"^(?=.*Button)(?!.*Text)(?!.*Sprite)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "schemaDefinitionsToLoad.json#/definitions/button" },
"^(?=.*Text)(?!.*Button)(?!.*Sprite)(?!.*Layout)(?!.*\\s).+$": { "$ref": "schemaDefinitionsToLoad.json#/definitions/text" },
"^(?=.*Layout)(?!.*Button)(?!.*Text)(?!.*Sprite)(?!.*\\s).+$": { "$ref": "schemaDefinitionsToLoad.json#/definitions/layout" },
"ResolutionToLoad": { "$ref": "schemaDefinitionsToLoad.json#/definitions/resolution"}
},
"additionalProperties": false
}

This is an example of the schemaToLoad.json I defined on my project. On patternProperties you define the main objects your layout is composed of. You can use particular names for unique cases (ResolutionToLoad object) or names following regular expressions to apply to different classes of objects (sprites, buttons, texts and layout containers).

These main objects definitions relate to each own case at schemaDefinitionsToLoad.json, where its needed properties and limitations are defined.

additionalProperties to false indicates that there can’t exist more main objects definitions besides the defined on patternProperties, if the layout file contains an object definition which doesn’t fit in any of these cases, an error will be thrown.

Defining the schemaDefinitionsToLoad.json

This is an example of the whole schemaDefinitionsToLoad.json:

{
"$id": "schemaDefinitionsToLoad.json",
"definitions":
{
"resolution":
{
"type":"object",
"properties":
{
"w": {
"type": "integer"
},
"h": {
"type": "integer"
}
},
"required": [ "w", "h" ],
"additionalProperties": false
},
"sprite":
{
"type":"object",
"properties":
{
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"w": {
"type": "integer"
},
"h": {
"type": "integer"
},
"z": {
"type": "integer"
}
},
"required": [ "x", "y", "w", "h" ],
"additionalProperties": false
},
"button":
{
"type":"object",
"properties":
{
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"w": {
"type": "integer"
},
"h": {
"type": "integer"
},
"z": {
"type": "integer"
}
},
"required": [ "x", "y", "w", "h" ],
"additionalProperties": false
},
"layout":
{
"type":"object",
"properties":
{
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"w": {
"type": "integer"
},
"h": {
"type": "integer"
},
"z": {
"type": "integer"
},
"scale": {
"type": "object",
"properties":
{
"x":{
"type": "number"
},
"y":{
"type": "number"
}
},
"required": [ "x", "y"],
"additionalProperties": false
}
},
"patternProperties": {
"^(?=.*Sprite)(?!.*Button)(?!.*Text)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "#/definitions/sprite" },
"^(?=.*Button)(?!.*Text)(?!.*Sprite)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "#/definitions/button" },
"^(?=.*Tag)(?!.*Text)(?!.*Button)(?!.*Sprite)(?!.*Layout)(?!.*\\s)[a-zA-Z].+$": { "$ref": "#/definitions/text" },
"^(?=.*Text)(?!.*Button)(?!.*Sprite)(?!.*Layout)(?!.*\\s).+$": { "$ref": "#/definitions/text" },
"^(?=.*Layout)(?!.*Button)(?!.*Text)(?!.*Sprite)(?!.*\\s).+$": { "$ref": "#/definitions/layout" }
},
"additionalProperties": false
},
"text":
{
"type":"object",
"properties":
{
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"w": {
"type": "integer"
},
"h": {
"type": "integer"
},
"z": {
"type": "integer"
},
"style":
{
"type": "object",
"properties":
{
"font":{
"type": "string",
"pattern": "^(\\d)+px\\s[a-zA-Z]+$"
},
"tint":{
"type": "string"
},
"align":{
"type": "string"
},
"lineType":{
"type": "string"
},
"filters":{
"type": "array"
},
"letterSpacing": {
"type": "integer"
}
},
"required": [ "font", "tint", "align" ],
"additionalProperties": false
}
},
"required": [ "x", "y", "w", "h" ],
"additionalProperties": false
}
}
}

Let’s check some pieces!

ResolutionToLoad case

ResolutionToLoad case was defined at schemaToLoad.json like:

"ResolutionToLoad": 
{
"$ref": "schemaDefinitionsToLoad.json#/definitions/resolution"
}

At schemaDefinitionsToLoad.json, we find its complete definition:

"resolution":
{
"type":"object",
"properties":
{
"w": {
"type": "integer"
},
"h": {
"type": "integer"
}
},
"required": [ "w", "h" ],
"additionalProperties": false
},

On type parameter we define if it’s a string, integer, object… at properties parameter we define the properties that ResolutionToLoad object should contain, on this case, w from width and h from height which are defined as required. Apart from w and h, ResolutionToLoad must not contain any additional properties.

This is an example of ResolutionToLoad object from the game’s layout, which must follow the rules we’ve defined on schemaToLoad.json and schemaDefinitionsToLoad.json:

"ResolutionToLoad": 
{
"w":1376,
"h":720
}

Layout containers case

The case for layout containers was defined using a regular expression at schemaToLoad.json:

"^(?=.*Layout)(?!.*Button)(?!.*Text)(?!.*Sprite)(?!.*\\s).+$": { "$ref": "schemaDefinitionsToLoad.json#/definitions/layout" },

At schemaDefinitionsToLoad.json, we find its complete definition:

"layout":
{
"type":"object",
"properties":
{
"x": {
"type": "integer"
},
"y": {
"type": "integer"
},
"w": {
"type": "integer"
},
"h": {
"type": "integer"
},
"z": {
"type": "integer"
},
"scale": {
"type": "object",
"properties":
{
"x":{
"type": "number"
},
"y":{
"type": "number"
}
},
"required": [ "x", "y"],
"additionalProperties": false
}
},
"patternProperties": {
"^(?=.*Sprite)(?!.*Button)(?!.*Text)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "#/definitions/sprite" },
"^(?=.*Button)(?!.*Text)(?!.*Sprite)(?!.*Layout)(?=.*_)(?!.*\\s)(?=.+[A-Z]).+$": { "$ref": "#/definitions/button" },
"^(?=.*Tag)(?!.*Text)(?!.*Button)(?!.*Sprite)(?!.*Layout)(?!.*\\s)[a-zA-Z].+$": { "$ref": "#/definitions/text" },
"^(?=.*Text)(?!.*Button)(?!.*Sprite)(?!.*Layout)(?!.*\\s).+$": { "$ref": "#/definitions/text" },
"^(?=.*Layout)(?!.*Button)(?!.*Text)(?!.*Sprite)(?!.*\\s).+$": { "$ref": "#/definitions/layout" }
},
"additionalProperties": false
},

This case is also a type object and has properties like x, y, z, w, h and scale. As you can see, scale property is defined as an object and “nested” with its own properties. The special thing of this case is that it also uses patternProperties like at schemaToLoad.json and referencing the definitions of the same file. The reason for this is because a layout container can also contain texts, sprites, buttons or others layouts.

Example of a layout element from the game’s layout:

"LayoutCoinsInfo": {
"SpriteCoin_CoinCounter": {
"x": 0,
"y": 0,
"w": 22,
"h": 32,
"z": 0
},
"TextCoinsNumber": {
"x": 30,
"y": 0,
"w": 100,
"h": 40,
"z": 0,
"style": {
"font": "33px Verdana",
"tint": "0xFFFFFF",
"align": "left"
}
}
}

Looking at this example should show you how flexible can be using JSON Schema. At first, it might seem confusing, but after reading the docs and playing a while you get how powerful it is and how you can apply it to your projects.

JSON Schema support

JSON Schema is available in JavaScript, .NET, C, Go, Java, PHP, Python… You can get the version you need through this page: http://json-schema.org/implementations.html

In my case I used JavaScript ‘ajv’: https://github.com/epoberezkin/ajv

JSON Schema JavaScript ajv implementation

This is the main function implementation I did on my project, giving user’s feedback of the layout checking process through Chrome’s console:

/**
* checkLayout checks layout.json according to the defined schemaToLoad.json and schemaDefinitionsToLoad.json
*
@param data
*/
checkLayout(data)
{
console.log('Checking layout schema errors...');
let validator = this._ajv.addSchema(this._schemaDefinitionsToLoadData, 'layoutSchema')
.compile(this._schemaToLoadData);

let context=this;

validator(data, context)
.then(function (resultData)
{
Utils.createLogMessage('%c >> 😊 Layout is following the schema defined successfully!! ', LOG_STYLES.SCHEMA_OK);
console.log('End of checking layout schema errors');
context.dispatch('layoutCheckedSuccessfully');
})
.catch(function (err)
{
if (!(err instanceof Ajv.ValidationError)) throw err;
// data is invalid
for(let i=0; i<err.errors.length; i++)
{
Utils.createLogMessage('%c >> 😱 ERROR '+i+' at layout object: '+err.errors[i].dataPath+' ', LOG_STYLES.SCHEMA_WRONG_TITLE);
Utils.createLogMessage('%c at object property or properties: '+JSON.stringify(err.errors[i].params), LOG_STYLES.SCHEMA_WRONG_INFO);
Utils.createLogMessage('%c REASON: '+err.errors[i].message+' ', LOG_STYLES.SCHEMA_WRONG_INFO);
console.log('\n');
}
});

}

--

--

Verónica Valls
Game & Frontend Development Stuff

Mobile & frontend developer for real world projects. Game designer/developer for my mind’s delirium ideas. Cats & dogs dietetical and nutritional advisor.