Schema validationSchema syntax

This document describes the language rules for writing your own Liveblocks schemas. It is an exhaustive account of all features that are implemented and supported as part of the the public beta and is open source.

We support: scalars, arrays, objects, optionals, LiveObject, LiveList, LiveMap, and most unions. We’re sharing our plans for other syntaxes so you can give us early feedback here.

Storage root

Each schema must include the Storage type, a special type of “root” object.

type Storage {


Familiar scalar types are globally available when you create a schema:

  • string
  • number
  • boolean
  • null

A sample schema using only scalar types could look like this:

type Storage {  name: string  age: number  hasSiblings: boolean  favoritePet: string | null}

And here are some updates that would be accepted and rejected by the schema:

// ✅ Valid storage updatesroot.set("name", "Marie Curie");root.set("age", 66);root.set("hasSiblings", true);root.set("favoritePet", "Cooper");root.set("favoritePet", null);
// ❌ Invalid storage updatesroot.set("name", true);root.set("hasSiblings", null);root.set("favoritePet", 0);

You can also use literal types to restrict values even further:

type Event {  statusCode: 200 | 400  info: string}
type Storage { theme: "light" | "dark" history: LiveList<Event>}


Each field inside an object type can be marked optional using the ? operator. An optional field means that it can be deleted.

For example, to make the age field optional:

type Storage {  name: string  age?: number  height: number  hasSiblings: boolean}

Accepted and rejected updates:

// ✅root.delete("age");
// ❌ Field 'name' is not optionalroot.delete("name");


Our language supports two different ways to declare object types:

  • Named object types
type Scientist {  name: string  age: number}
type Storage { scientist: Scientist}
  • Anonymous object types (inlined)
type Storage  {  scientist: { name: string, age: number }}

These definitions are equivalent. Accepted and rejected updates:

// ✅root.set("scientist", { name: "Marie Curie", age: 66 });
// ❌ Required field 'age' is missingroot.set("scientist", { name: "Marie Curie" });


To use an object type definition as a “live” object, wrap it in the built-in LiveObject construct, like so:

type Scientist {  name: string  age: number}
type Storage { scientist: LiveObject<Scientist> // ^^^^^^^^^^}

Accepted and rejected updates:

// ✅root.set("scientist", new LiveObject({ name: "Marie Curie"; age: 66 }));
// ❌ Should be a LiveObjectroot.set("scientist", { name: "Marie Curie"; age: 66 });


Arrays can be defined like this:

type Storage {  animals: string[]}

Accepted and rejected updates:

// ✅root.set("animals", ["🦁", "🦊", "🐵"]));
// ❌ Should contain stringsroot.set("animals", [1, 2, 2]);


To use a “live” array instead of a normal array, wrap your item type in a LiveList when you reference it.

For example:

type Storage {  animals: LiveList<string>  //       ^^^^^^^^}

Accepted and rejected updates:

// ✅root.set("animals", new LiveList(["🦁", "🦊", "🐵"]));
// ❌ Should be a LiveListroot.set("animals", ["🦁", "🦊", "🐵"]);


It’s also possible to define a LiveMap in your schema.

For example:

type Shape {  x: number  y: number  fill: "red" | "yellow" | "blue"}
type Storage { shapes: LiveMap<string, Shape> // ^^^^^^^}

The first argument to a LiveMap construct must always be string.

Accepted and rejected updates:

// ✅root.set(  "shapes",  new LiveMap([["shapeId", { x: 100, y: 100, fill: "blue" }]]));
// ❌ Required field 'fill' is missingroot.set("shapes", new LiveMap([["shapeId", { x: 100, y: 100 }]]));


You can model a choice between two types using a union, which will be familiar from TypeScript. Here are some examples:

type Storage {  ids: (string | number)[]  selectedId: string | null  person: LiveObject<Person> | null  people: LiveList<LiveObject<Person>> | null}

What’s to come

We’re also planning to support more language features. Discriminated unions, regex, ranges, etc...

If you’re interested in a specific feature, please send your feedback on this GitHub discussion so we can prioritize it appropriately!