With Valibot
Using doba with Valibot schemas and migration context.
Using Valibot for schema validation with legacy user migration and context tracking.
Full example
import * as v from 'valibot'
import { createRegistry } from 'dobajs'
const databaseUser = v.object({
id: v.string(),
email: v.pipe(v.string(), v.email()),
passwordHash: v.string(),
createdAt: v.pipe(v.string(), v.isoTimestamp()),
settings: v.object({
theme: v.picklist(['light', 'dark']),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
}),
internal: v.object({
lastLoginIp: v.string(),
failedAttempts: v.number(),
}),
}),
})
const frontendUser = v.object({
id: v.string(),
email: v.pipe(v.string(), v.email()),
createdAt: v.pipe(v.string(), v.isoTimestamp()),
settings: v.object({
theme: v.picklist(['light', 'dark']),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
}),
}),
})
const aiUser = v.object({
id: v.string(),
email: v.string(),
theme: v.string(),
hasNotifications: v.boolean(),
})
const legacyUser = v.object({
name: v.optional(v.string()),
darkMode: v.optional(v.boolean()),
})
const registry = createRegistry({
schemas: {
database: databaseUser,
frontend: frontendUser,
ai: aiUser,
legacy: legacyUser,
},
migrations: {
'database->frontend': (user) => ({
id: user.id,
email: user.email,
createdAt: user.createdAt,
settings: {
theme: user.settings.theme,
notifications: user.settings.notifications,
},
}),
'database->ai': (user) => ({
id: user.id,
email: user.email,
theme: user.settings.theme,
hasNotifications: user.settings.notifications.email || user.settings.notifications.push,
}),
'frontend->ai': (user) => ({
id: user.id,
email: user.email,
theme: user.settings.theme,
hasNotifications: user.settings.notifications.email || user.settings.notifications.push,
}),
'legacy->frontend': (user, ctx) => {
ctx.defaulted(['id'], 'generated new id')
ctx.defaulted(['createdAt'], 'set to current timestamp')
ctx.defaulted(['settings', 'notifications'], 'defaulted to all false')
let email = 'unknown@example.com'
if (typeof user.name === 'string' && user.name.length > 0) {
email = `${user.name.toLowerCase().replace(/\s+/g, '.')}@legacy.example.com`
ctx.warn(`converted name "${user.name}" to email`)
}
return {
id: `legacy-${Date.now()}`,
email,
createdAt: new Date().toISOString(),
settings: {
theme: user.darkMode === true ? 'dark' : 'light',
notifications: { email: false, push: false },
},
}
},
},
})Usage
// Direct transform
const dbUser = {
/* ... */
}
const frontend = await registry.transform(dbUser, 'database', 'frontend')
// Legacy migration with context
const legacy = { name: 'Alice Johnson', darkMode: true }
const result = await registry.transform(legacy, 'legacy', 'frontend')
if (result.ok) {
console.log(result.value)
console.log(result.meta.defaults) // tracked defaulted fields
console.log(result.meta.warnings) // warnings from ctx.warn()
}
// Multi-hop: legacy -> frontend -> ai
const ai = await registry.transform(legacy, 'legacy', 'ai')The migration context (ctx) lets you track defaults and warnings without throwing. This metadata
is available on result.meta after transform.