6. Schemas & Validation

Defining and validating the structure of your Records

🚧

This tutorial needs review

Introduction

This pages assumes you've read part 5 of the tutorial, Saving data.

A Schema represents how your data is organized. The following JSData functionalities require that you define a Schema:

  • Change Detection
  • Validation
  • Strict JSONification of Records

JSData Schemas following an extended version of http://json-schema.org/. Let's get started.

Defining a schema

Schemas are defined on a per-Mapper basis. One Schema per Mapper. A Mapper represents the CRUD operations that can be performed on a Resource, and its Schema represents the form that the Records of the Mapper will take. There are several ways to define a Schema:

import { Schema, Container } from 'js-data';

const personSchema = new Schema({
  $schema: 'http://json-schema.org/draft-04/schema#', // optional
  title: 'Person',                                    // optional
  description: 'Schema for Person Records.',          // optional

  type: 'object', // required
  properties: {
    name: { type: 'string' }
  }
});

const store = new Container();

store.defineMapper('person', {
  schema: personSchema
});
import { Container } from 'js-data';

const store = new Container();

store.defineMapper('person', {
  // here we define the schema inline, but you can also
  // provide a reference to a standalone schema
  schema: {
    $schema: 'http://json-schema.org/draft-04/schema#', // optional
    title: 'Person',                                    // optional
    description: 'Schema for Person Records.',          // optional
    
    type: 'object', // required
    properties: {
      name: { type: 'string' }
    }
  }
});
import { Mapper } from 'js-data';

const personService = new Mapper({ 
  name: 'person',
  // here we define the schema inline, but you can also
  // provide a reference to a standalone schema
  schema: {
    $schema: 'http://json-schema.org/draft-04/schema#', // optional
    title: 'Person',                                    // optional
    description: 'Schema for Person Records.',          // optional
    
    type: 'object', // required
    properties: {
      name: { type: 'string' }
    }
  }
});

Schema Types

JSON Schema defines seven primitive types for JSON values:

  • array - A JSON array.
  • boolean - A JSON boolean.
  • integer - A JSON number without a fraction or exponent part.
  • number - Any JSON number. Number includes integer.
  • null - The JSON null value.
  • object - A JSON object.
  • string - A JSON string.

Here's a more complex example:

import { Schema } from 'js-data';

const productSchema = new Schema({
  $schema: 'http://json-schema.org/draft-04/schema#',
  title: 'Product',
  description: 'A product from Acme\'s catalog',
  type: 'object',
  properties: {
    id: {
      description: 'The unique identifier for a product',
      type: 'number'
    },
    name: { type: 'string' },
    price: { type: 'number' },
    tags: {
      type: 'array',
      items: { type: 'string' }
    },
    dimensions: {
      type: 'object',
      properties: {
        length: { type: 'number' },
        width: { type: 'number' },
        height: { type: 'number' }
      },
      required: ['length', 'width', 'height']
    },
    warehouseLocation: {
      description: 'Coordinates of the warehouse with the product',
      type: 'object',
      properties: {
        latitude: { type: 'number' },
        longitude: { type: 'number' }
      }
    }
  },
  required: ['id', 'name', 'price']
});

πŸ‘

Tip

To JSData, a value of undefined means the property does not exist. To set a property's value to "no value", set it to undefined. null on the hand means the property exists and has the value of null. If a property can take a value of null, then make a note of that in the schema:

name: { type: ['string', 'null'] }

Type Keywords

Assigning types to a property enables certain validation keywords to be used for the property. For example, if a name property has a type of string, then the maxLength keyword can be used to invalidate the property if its value's length exceeds the specified limit. The maxLength keyword doesn't make any sense in the context of a number type, and therefore will have no effect if a property's value is of type number.

Here are the keywords available to each type:

  • array: items, maxItems, minItems, and uniqueItems
  • number and integer: multipleOf, maximum, and minimum
  • object: maxProperties, minProperties, required, properties, additionalProperties , patternProperties, and dependencies
  • string: maxLength, minLength, and pattern

The following keywords are available for all types: enum, type, allOf, anyOf, oneOf, and not.

πŸ‘

Tip

Read more about the available Type Validation Keywords at http://json-schema.org/latest/json-schema-validation.html

Let's add some keywords to the Product Schema example:

import { Schema } from 'js-data';

const productSchema = new Schema({
  $schema: 'http://json-schema.org/draft-04/schema#',
  title: 'Product',
  description: 'A product from Acme\'s catalog',
  type: 'object',
  properties: {
    id: {
      description: 'The unique identifier for a product',
      type: 'number',
      minimum: 1
    },
    name: {
      type: 'string',
      maxLength: 255
    },
    price: {
      type: 'number',
      minimum: 0,
      exclusiveMinimum: true
    },
    tags: {
      type: 'array',
      items: { type: 'string' },
      minItems: 1,
      uniqueItems: true
    },
    dimensions: {
      type: 'object',
      properties: {
        length: { type: 'number' },
        width: { type: 'number' },
        height: { type: 'number' }
      },
      required: ['length', 'width', 'height']
    },
    warehouseLocation: {
      description: 'Coordinates of the warehouse with the product',
      type: 'object',
      properties: {
        latitude: { type: 'number' },
        longitude: { type: 'number' }
      }
    }
  },
  required: ['name', 'price']
});

Validating records

With a Schema you can validate any value against the Schema:

import { Schema } from 'js-data';

const personSchema = new Schema({
  type: 'object', // required
  properties: {
    name: { type: 'string' }
  }
});

// These are invalid!
personSchema.validate('foo'); // [{...}]
personSchema.validate(1234); // [{...}]
personSchema.validate(['bar']); // [{...}]

// Success!
personSchema.validate({ name: 'John' }); // undefined
import { Schema, Container } from 'js-data';

const personSchema = new Schema({
  type: 'object', // required
  properties: {
    name: { type: 'string' }
  }
});
const store = new Container();
store.defineMapper('person', {
  schema: personSchema
});

// These are invalid!
store.validate('person', 'foo'); // [{...}]
store.validate('person', 1234); // [{...}]
store.validate('person', ['bar']); // [{...}]

// Success!
store.validate('person', { name: 'John' }); // undefined
import { Schema, Mapper } from 'js-data';

const personSchema = new Schema({
  type: 'object', // required
  properties: {
    name: { type: 'string' }
  }
});
const personService = new Mapper({
  name: 'person',
  schema: personSchema
});

// These are invalid!
personService.validate('foo'); // [{...}]
personService.validate(1234); // [{...}]
personService.validate(['bar']); // [{...}]

// Success!
personService.validate({ name: 'John' }); // undefined
// Skip validation on instantiation
const person = store.createRecord('person',{
  name: 'John'
}, {
  noValidate: true
});

console.log(person.validate()); // [{...}]

person.name = 'John';

console.log(person.validate()); // undefined
const person = store.createRecord('person',{
  name: 'John'
});

person.name = 123; // Throws an error

Records validate themselves upon instantiation:

// These are invalid!
try {
  store.createRecord('person', { name: 1234 });
} catch (err) {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
}

// Success!
store.createRecord('person', { name: 'John' });
import { Container, Record, Schema } from 'js-data';

const personSchema = new Schema({
  type: 'object', // required
  properties: {
    name: { type: 'string' }
  }
});

const store = new Container();

class PersonRecord extends Record {}
store.defineMapper('person', {
  schema: personSchema,
  recordClass: PersonRecord
});

let person;

// These are invalid!
try {
  person = new PersonRecord({ name: 1234 });
} catch (err) {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
}

// Success!
person = new PersonRecord({ name: 'John' });

Records also validate properties on assignment:

// Success!
const person = store.createRecord('person', { name: 'John' });

try {
  person.name = 1234;
} catch (err) {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
}

To disable property validation on assignment, set Mapper#validateOnSet to false, or pass validateOnSet: false to createRecord() or add().

πŸ‘

Tip

You can disable validation on assignment by setting Mapper#validateOnSet to false.

Records can also tell you whether they are currently in a valid state:

// Skip validation on instantiation
const person = store.createRecord('person',{
  name: 'John'
}, {
  noValidate: true
});

console.log(person.isValid()); // false

person.name = 'John';

console.log(person.isValid()); // true

Configuring validation hooks

JSData automatically calls Schema#validate for you when you try to create or update records via an adapter, for example:

store.create('person', {
  name: 1234
}).catch((err) => {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
});
store.createMany('person', [{
  name: 1234
}, {
  name: 'Sally'
}).catch((err) => {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
});
store.update('person', 1234567890, {
  name: 1234
}).catch((err) => {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
});
store.updateAll('comment', {
  status: 'flagged'
}, {
  user_id: 123457909
}).catch((err) => {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
});
store.updateMany('person', [{
  id: 1234567890,
  name: 1234
}, {
  id: 4736583920,
  name: 'Sally'
}).catch((err) => {
  console.log(err.message); // "validation failed"
  console.log(err.errors); // [{...}]
});

πŸ‘

Tip

You can skip validation by passing noValidate: true to the method call.

Custom validation

Extending the Schema class

TODO

Validation Keywords:

TODO

Types:

TODO

Type Group Validators:

TODO

Extending Mapper#validate

TODO

class MyMapper extends Mapper {
  validate (...args) {
    // do some custom validation
    if (/* some condition */) {
      return [{ expected: 'some expectation', actual: 'actual value' }];
    }
    // resume default behavior
    return super.validate(...args);
}

🚧

See an issue with this tutorial?

You can open an issue or better yet, suggest edits right on this page.


What’s Next

Continue with part 7 of the Tutorial or explore other documentation.