apps/docs/content/sdk-features/validation.mdx
Validation in tldraw is handled by @tldraw/validate and applied across schemas, record types, and shape props. Validators enforce runtime type safety and provide structured errors when data is malformed.
RecordPropscreateRecordType and StoreSchemaparseRequestBody and parseRequestQueryUse T validators to describe data shapes and validate unknown input:
import { T } from '@tldraw/validate'
const userValidator = T.object({
id: T.string,
name: T.string.optional(),
age: T.number.optional(),
})
const user = userValidator.validate(input)
Every validator has three key methods:
validate(value) - validates unknown input and returns a typed resultisValid(value) - returns true if valid, false otherwise (useful as a type guard)validateUsingKnownGoodVersion(knownGood, newValue) - performance-optimized validation that reuses previously validated dataT.unknown, T.anyT.string, T.number, T.boolean, T.bigintT.positiveNumber, T.nonZeroNumber, T.nonZeroFiniteNumberT.unitInterval, T.integer, T.positiveInteger, T.nonZeroIntegerT.array, T.arrayOf, T.object, T.unknownObjectT.dict, T.jsonDict, T.jsonValueT.literal, T.literalEnum, T.setEnumT.union, T.orT.linkUrl, T.srcUrl, T.httpUrl, T.indexKeyvalidator.optional(), validator.nullable()T.optional(...), T.nullable(...)validator.refine(...), validator.check(...)T.model(...)import { T, ValidationError } from '@tldraw/validate'
const configValidator = T.object({
id: T.string,
mode: T.literalEnum('view', 'edit'),
tags: T.arrayOf(T.string).optional(),
meta: T.object({ note: T.string }).nullable(),
})
const evenNumber = T.number.check('even', (value) => {
if (value % 2 !== 0) throw new ValidationError('Expected even number')
})
Shapes and bindings use RecordProps to validate their props at runtime. This keeps stored data consistent with the schema.
import { ShapeUtil, type RecordProps, T, DefaultColorStyle } from 'tldraw'
class MyShapeUtil extends ShapeUtil<MyShape> {
static override props: RecordProps<MyShape> = {
color: DefaultColorStyle,
text: T.string,
}
}
The Store validates records on write. You can provide onValidationFailure to recover or sanitize data:
import { StoreSchema, createRecordType } from '@tldraw/store'
const Book = createRecordType<Book>('book', { scope: 'document' })
const schema = StoreSchema.create(
{ book: Book },
{
onValidationFailure: (failure) => failure.record,
}
)
The failure object contains:
| Property | Description |
|---|---|
error | The validation error that occurred |
store | The store instance where validation failed |
record | The invalid record |
phase | When validation failed: 'initialize', 'createRecord', 'updateRecord', or 'tests' |
recordBefore | The previous record state (null for new records) |
onValidationFailure receives the failure object.The ValidationError class provides structured information about what went wrong:
import { T, ValidationError } from '@tldraw/validate'
const userValidator = T.object({
name: T.string,
settings: T.object({ theme: T.literalEnum('light', 'dark') }),
})
try {
userValidator.validate({ name: 'Alice', settings: { theme: 'invalid' } })
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.message) // "At settings.theme: Expected "light" or "dark", got "invalid""
console.log(error.rawMessage) // 'Expected "light" or "dark", got "invalid"'
console.log(error.path) // ['settings', 'theme']
}
}
The error has two useful properties:
rawMessage - the error message without path informationpath - an array showing where in the data structure validation failed (e.g., ['settings', 'theme'] or ['items', 0, 'name'])The full message property combines these: "At settings.theme: Expected...".
onValidationFailure, return a valid record or rethrow to abort the write.