LogoRobo.js

Schema validation

Validate state updates with built-in schemas or Zod.

Schema validation ensures state updates conform to an expected structure before being broadcast. Export a schema from any sync handler to enable automatic validation. Supports both a built-in schema format and Zod.

Built-in schema

Define schemas as a record of field definitions. No external dependencies required.

src/sync/game/[roomId]/state.ts
import type { BuiltInSchema } from '@robojs/sync/server'

export const schema: BuiltInSchema = {
	score: { type: 'number', min: 0 },
	phase: { type: 'string', enum: ['lobby', 'playing', 'ended'] },
	winner: { type: 'string', nullable: true, optional: true },
	players: { type: 'object' }
}
src/sync/game/[roomId]/state.js

export const schema = {
	score: { type: 'number', min: 0 },
	phase: { type: 'string', enum: ['lobby', 'playing', 'ended'] },
	winner: { type: 'string', nullable: true, optional: true },
	players: { type: 'object' }
}

SchemaField reference

FieldTypeDescription
type'string' | 'number' | 'boolean' | 'object' | 'array'Field type (required)
nullablebooleanAllow null values
optionalbooleanAllow undefined / missing values
minnumberMinimum value (numbers)
maxnumberMaximum value (numbers)
minLengthnumberMinimum length (strings, arrays)
maxLengthnumberMaximum length (strings, arrays)
patternstringRegex pattern (strings)
enum(string | number)[]Allowed values
itemsSchemaFieldSchema for array elements
propertiesRecord<string, SchemaField>Nested schema for object properties

Advanced example

Nested validation with arrays and objects.

src/sync/game/[roomId]/state.ts
import type { BuiltInSchema } from '@robojs/sync/server'

export const schema: BuiltInSchema = {
	name: { type: 'string', minLength: 1, maxLength: 50 },
	health: { type: 'number', min: 0, max: 100 },
	inventory: {
		type: 'array',
		maxLength: 20,
		items: {
			type: 'object',
			properties: {
				id: { type: 'string' },
				quantity: { type: 'number', min: 1 }
			}
		}
	},
	position: {
		type: 'object',
		properties: {
			x: { type: 'number', min: 0, max: 1000 },
			y: { type: 'number', min: 0, max: 1000 }
		}
	}
}
src/sync/game/[roomId]/state.js

export const schema = {
	name: { type: 'string', minLength: 1, maxLength: 50 },
	health: { type: 'number', min: 0, max: 100 },
	inventory: {
		type: 'array',
		maxLength: 20,
		items: {
			type: 'object',
			properties: {
				id: { type: 'string' },
				quantity: { type: 'number', min: 1 }
			}
		}
	},
	position: {
		type: 'object',
		properties: {
			x: { type: 'number', min: 0, max: 1000 },
			y: { type: 'number', min: 0, max: 1000 }
		}
	}
}

Zod schemas

If you prefer Zod, export a Zod schema instead. The validation system detects Zod schemas automatically via the .safeParse method.

src/sync/game/[roomId]/state.ts
import { z } from 'zod'

export const schema = z.object({
	score: z.number().min(0),
	phase: z.enum(['lobby', 'playing', 'ended']),
	winner: z.string().nullable().optional(),
	players: z.record(z.object({
		name: z.string(),
		ready: z.boolean()
	}))
})
src/sync/game/[roomId]/state.js
import { z } from 'zod'

export const schema = z.object({
	score: z.number().min(0),
	phase: z.enum(['lobby', 'playing', 'ended']),
	winner: z.string().nullable().optional(),
	players: z.record(z.object({
		name: z.string(),
		ready: z.boolean()
	}))
})

Zod is not included as a dependency. Install it separately if you want to use Zod schemas.

Validation errors

When validation fails, the update is rejected. For direct state updates, the client receives a validation_error message. For RPC calls, the error is returned in the CallResult:

const result = await call('action', payload)
if (!result.success) {
	console.log('Validation failed:', result.error) // 'schema_validation_failed'
}
const result = await call('action', payload)
if (!result.success) {
	console.log('Validation failed:', result.error) // 'schema_validation_failed'
}

Combining with validate

Schema validation runs before the custom validate function. Both must pass for the update to be accepted.

src/sync/game/[roomId]/state.ts
import type { BuiltInSchema, SyncUpdateContext } from '@robojs/sync/server'

export const schema: BuiltInSchema = {
	score: { type: 'number', min: 0 }
}

export function validate(ctx: SyncUpdateContext): boolean | string {
	// Schema already ensured score is a non-negative number
	// Additional business logic here
	if (ctx.newState.score > ctx.oldState?.score + 10) {
		return 'score_increase_too_large'
	}
	return true
}
src/sync/game/[roomId]/state.js

export const schema = {
	score: { type: 'number', min: 0 }
}

export function validate(ctx) {
	// Schema already ensured score is a non-negative number
	// Additional business logic here
	if (ctx.newState.score > ctx.oldState?.score + 10) {
		return 'score_increase_too_large'
	}
	return true
}

Next Steps

On this page