Documentation

Introduction

Give a collection a schema and every insert, update, replace and upsert must satisfy it. Bad data never gets in, and you learn about every problem at once.

app.db.createCollection('users', {
  required: ['name', 'email'],
  additionalProperties: false,
  properties: {
    name: { type: 'string', minLength: 2, maxLength: 80 },
    email: { type: 'string', format: 'email' },
    age: { type: ['integer', 'null'], minimum: 0, maximum: 150 },
    role: { enum: ['admin', 'user'], default: 'user' },
    tags: { type: 'array', items: { type: 'string' }, uniqueItems: true, maxItems: 10 },
    createdAt: { type: 'date' },
    address: {
      type: 'object',
      required: ['zip'],
      properties: {
        zip: { type: 'string', pattern: '^\\d{5}-\\d{3}$' },
        city: { type: 'string', default: 'Recife' }
      }
    }
  }
});

app.db.users.insertOne({ name: 'Ana', email: 'ana@example.com', address: { zip: '50000-000' } });
// stored as { name, email, address: { zip, city: 'Recife' }, role: 'user', _id }

When it fails

try {
  app.db.users.insertOne({ name: 'A', email: 'not an email', extra: true });
} catch (error) {
  error instanceof sorla.ValidationError;   // true
  error.name;      // 'ValidationError'
  error.message;   // 'Invalid document: name must have at least 2 characters (and 2 more)'
  error.errors;
  // [
  //   { path: 'name',    rule: 'minLength', message: 'name must have at least 2 characters' },
  //   { path: 'email',   rule: 'format',    message: 'email must be a valid email' },
  //   { path: 'extra',   rule: 'additionalProperties', message: 'extra is not allowed' }
  // ]
}

Each issue has the field path (address.zip, tags[1]), the rule that failed and a readable message. insertMany, updateMany and bulkWrite operations stay atomic: one invalid document stores nothing in that call.


Keywords

Keyword Applies to Notes
type any string, number, integer, boolean, object, array, null, date (a Date object), or several: ['string', 'null']
required objects field names that must be present
properties objects a schema per field, nested at any depth
additionalProperties objects false rejects unknown fields; a schema validates them. _id is always allowed
enum any the value must be one of the list
const any the value must be exactly this
minimum, maximum numbers inclusive limits
exclusiveMinimum, exclusiveMaximum numbers exclusive limits
minLength, maxLength strings in characters (emoji count as one)
pattern strings a regular expression (string) the value must match
format strings email, date (YYYY-MM-DD), date-time (ISO 8601), uri
items arrays the schema of every item
minItems, maxItems arrays
uniqueItems arrays no repeated items
default any filled in on insert and upsert when the field is missing, also inside nested objects
title, description, examples, $schema, $id any annotations, ignored by validation (forms use title and description)

Unknown keywords, types and formats throw when the schema is set, so a typo such as tpye or format: 'e-mail' is caught right away instead of silently validating nothing.


Defaults

app.db.createCollection('tasks', {
  properties: {
    done: { type: 'boolean', default: false },
    tags: { type: 'array', default: [] },
    meta: { type: 'object', properties: { priority: { type: 'integer', default: 3 } } }
  }
});
app.db.tasks.insertOne({ title: 'x', meta: {} });
// { title: 'x', meta: { priority: 3 }, done: false, tags: [], _id }

Defaults are copied (each document gets its own array or object) and are applied before validation, so a default counts for required. Updates do not fill defaults; upserts do.


A function as schema

When rules depend on several fields, use a function. It returns true (or nothing) when the document is valid, otherwise false, a message, or a list of messages or { path, message } objects:

app.db.createCollection('orders', order => {
  const problems = [];
  if (order.total < 0) problems.push({ path: 'total', message: 'total must not be negative' });
  if (order.status === 'shipped' && !order.trackingCode) problems.push('shipped orders need a tracking code');
  return problems.length ? problems : true;
});

The issues have rule: 'custom'. Function schemas have no defaults.


Checking without storing

app.db.users.validate({ name: 'Ana' });
// { valid: false, errors: [{ path: 'email', rule: 'required', message: 'email is required' }] }

app.db.users.applyDefaults({ name: 'Ana' });
// a copy with the defaults: { name: 'Ana', role: 'user' }

validate is what forms use to show errors as the user types. applyDefaults shows what insertOne would store.


Changing the schema

app.db.users.setSchema(newSchema);   // returns the collection; affects the next writes
app.db.users.setSchema(null);        // no schema
app.db.users.getSchema();            // the schema you gave, or null

Existing documents are not revalidated when the schema changes.