--- title: Validation created_at: 12/19/2025 updated_at: 12/19/2025 keywords: - validation - validators - schema - records - errors status: published date: 12/19/2025 readability: 8 voice: 8 completeness: 9 accuracy: 9 notes: Add more API links. Use more contractions for natural flow. --- The `@tldraw/validate` package handles validation across tldraw's schemas, record types, and shape props. Validators enforce runtime type safety and provide structured errors when data is malformed. The `T` namespace is re-exported from `tldraw`, so `import { T } from 'tldraw'` works too. ## Where validation runs Validation runs whenever a record is written to the [store](/sdk-features/store): - Shape and binding props via [RecordProps](?) - Shape, binding, and user `meta` via the `meta` field on their schema config - Custom record types via [CustomRecordInfo](?) - Any record type in a store via [createRecordType](?) and [StoreSchema](?) If validation fails, the write throws and nothing is stored. ## Core validators Use `T` validators to describe data shapes and validate unknown input: ```typescript 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 result - `isValid(value)` returns true if valid, false otherwise (useful as a type guard) - `validateUsingKnownGoodVersion(knownGood, newValue)` reuses previously validated data to skip unchanged parts. The store calls this automatically when updating an existing record. ## Validator catalog | Category | Validators | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Primitives | `T.unknown`, `T.any`, `T.string`, `T.number`, `T.boolean`, `T.bigint` | | Numbers | `T.positiveNumber`, `T.nonZeroNumber`, `T.nonZeroFiniteNumber`, `T.unitInterval`, `T.integer`, `T.positiveInteger`, `T.nonZeroInteger` | | Collections | `T.array`, `T.arrayOf`, `T.object`, `T.unknownObject`, `T.dict`, `T.jsonDict`, `T.jsonValue` | | Unions | `T.literal`, `T.literalEnum`, `T.setEnum`, `T.union`, `T.numberUnion`, `T.or` | | URLs and IDs | `T.linkUrl`, `T.srcUrl`, `T.httpUrl`, `T.indexKey` | | Modifiers | `.optional()`, `.nullable()`, `.refine()`, `.check()`, `T.optional()`, `T.nullable()`, `T.model()` | Object validators also have `.extend()` to add fields and `.allowUnknownProperties()` to tolerate extra keys. ## Common validator patterns ```typescript import { T } 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 T.ValidationError('Expected even number') }) ``` ## Record props validation Shapes and bindings use [RecordProps](?) to validate their `props` at runtime. Each key maps to a validator, and the store rejects any write whose props don't pass: ```typescript import { DefaultColorStyle, RecordProps, T, TLBaseShape, TLDefaultColorStyle } from 'tldraw' type CardShape = TLBaseShape<'card', { color: TLDefaultColorStyle; text: string }> const cardShapeProps: RecordProps = { color: DefaultColorStyle, text: T.string, } ``` Assign this object to `static override props` on your `ShapeUtil`. See the [custom shape example](/examples/shapes/tools/custom-shape) for the full util. ## Store validation and recovery The store validates records on write. When you build your own [StoreSchema](?) you can pass `onValidationFailure` to recover or sanitize data instead of throwing: ```typescript import { BaseRecord, RecordId, StoreSchema, createRecordType } from '@tldraw/store' import { T, idValidator } from 'tldraw' interface Book extends BaseRecord<'book', RecordId> { title: string } const Book = createRecordType('book', { scope: 'document', validator: T.object({ id: idValidator>('book'), typeName: T.literal('book'), title: T.string, }), }) const schema = StoreSchema.create( { book: Book }, { onValidationFailure: (failure) => failure.record, } ) ``` The handler must return a valid record, or rethrow to abort the write. The `failure` object is a [StoreValidationFailure](?): | Property | Description | | -------------- | ---------------------------------------------------------------------------------------- | | `error` | The error that was thrown | | `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) | tldraw's own schema from [createTLSchema](?) (and so [createTLStore](?) and the `Tldraw` component) uses a built-in handler that reports the error and rethrows. You can't override it there; if you need recovery, build the `StoreSchema` yourself. ## Error handling `T.ValidationError` carries structured information about what went wrong: ```typescript import { T } 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 T.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'] } } ``` `rawMessage` is the message without path information, and `path` is an array showing where in the data structure validation failed (for example `['items', 0, 'name']`). The full `message` combines them. Validators must be pure and must not mutate input values. ## Related examples - [Custom shape](/examples/shapes/tools/custom-shape) - Define shape props with validators using RecordProps. - [Custom validators for shape props](/examples/shapes/tools/custom-validators) - Add constraints with `.check()` and `.refine()`.