ZodSchemer
ZodSchemer requires the zod dependency (v3) to be installed as peer dependency:
npm install zod@v3
Transpiles DynamoDB-Toolbox schemas to Zod Schemas. Note that the transpilation itself is type-safe, which means that resulting schema types can be introspected and type inference is preserved:
import { ZodSchemer } from 'dynamodb-toolbox/schema/actions/zodSchemer'
const zodSchemer = pokemonSchema.build(ZodSchemer)
const zodParser = zodSchemer.parser()
const parsed = zodParser.parse(input)
// => TransformedValue<typeof pokemonSchema>
const zodFormatter = zodSchemer.formatter()
const formatted = zodFormatter.parse(parsed)
// => FormattedValue<typeof pokemonSchema>
Because DynamoDB-Toolbox schema are more flexible than Zod Schemas (Parsing vs Formatting, Item vs Keys, Updates syntax etc.), you may need to transpile several Zod Schemas for different contexts.
You can also derive schemas from Zod schemas with the [fromZodSchema] util:
import { z } from 'zod'
import { fromZodSchema } from 'dynamodb-toolbox/schema/actions/fromZodSchema'
const zodSchema = z.object({
firstName: z.string(),
lastName: z.string()
})
const ddbToolboxSchema = fromZodSchema(zodSchema)
Again the transpilation itself is type-safe and type inference is preserved, although the resulting schema may have to be tweaked to introduce DynamoDB related features (keys, links, transforms etc.).
β οΈ Known Limitationsβ
ZodSchemerβ
The most important limitation at the moment is that links are not transpiled (but defaults are):
const nameSchema = item({
firstName: string(),
lastName: string()
})
const fullSchema = nameSchema.and({
fullName: string().link<typeof nameSchema>(
({ firstName, lastName }) =>
[firstName, lastName].join(' ')
)
}))
const zodSchema = fullSchema.build(ZodSchemer).parser()
// β Error: Missing `fullName` attribute
zodSchema.parse({ firstName: 'John', lastName: 'Dow' })
Also, hidden attributes are simply stripped for now (thus not validated during formatting):
const schema = map({
hidden: string().hidden()
})
const zodSchema = schema.build(ZodSchemer).formatter()
// β οΈ Does not throw even if `hidden` is required
zodSchema.parse({})
Due to some discrepancies between schema systems, discriminated unions also suffer from some minor limitations:
- Discriminated AND remapped
mapscannot be transpiled:
const schema = anyOf(
map({ type: string().const('a').savedAs('_t') }),
map({ type: string().const('b').savedAs('_t') })
).discriminate('type')
const zodSchema = schema.build(ZodSchemer).parser()
// β Fails: `ZodDiscriminatedUnion` does not support `ZodEffect` options
zodSchema.parse(input)
- Discriminated AND refined
mapscannot be transpiled (should be fixed with Zod v4):
const schema = anyOf(
map({ type: string().const('a') }).validate(...),
map({ type: string().const('b') }).validate(...)
).discriminate('type')
const zodSchema = schema.build(ZodSchemer).parser()
// β Fails: `ZodDiscriminatedUnion` does not support `ZodEffect` options
zodSchema.parse(input)
- Nested discriminated union cannot be transpiled (should be fixed with Zod v4):
const schema = anyOf(
map({ type: string().const('a') }),
anyOf(
map({ type: string().const('b') }),
map({ type: string().const('c') })
)
).discriminate('type')
const zodSchema = schema.build(ZodSchemer).parser()
// β Fails: `ZodDiscriminatedUnion` does not support `ZodDiscriminatedUnion` options
zodSchema.parse(input)
fromZodSchemaβ
Binary schemas (Uint8Array) do not exist in zod and are cannot be transpiled (see this issue).
Refinments are transpiled but transforms are not (should be fixed with Zod v4 codecs):
const zodSchema = z
.string()
.validate(value => value.length > 0)
const schema = fromZodSchema(zodSchema)
schema.build(Parser).parse('') // β Fails
const zodSchema = z.transform(value => value.trim())
const schema = fromZodSchema(zodSchema)
schema.build(Parser).parse(' foo ') // => ' foo '
Preprocessed schemas and intersections are not supported, as well as types that are not natively available in DynamoDB are (Dates, NaNs, InstanceOf etc.).
Methodsβ
parser(...)β
(options?: ZodParserOptions) => ZodParser<SCHEMA>
Creates a zodSchema to use for Item parsing. It includes defaults, custom validation and transformations (see the Parser action for more details on parsing):
import { z } from 'zod'
const nameSchema = item({
firstName: string().savedAs('_f'),
lastName: string().validate(input => input.length > 1)
})
const zodParser = nameSchema.build(ZodSchemer).parser()
// β `firstName` missing
zodParser.parse({ lastName: 'Dow' })
// β `lastName` too short
zodParser.parse({ firstName: 'John', lastName: '' })
// β
Success
zodParser.parse({ firstName: 'John', lastName: 'Dow' })
type ParsedName = z.infer<typeof zodParser>
// => { _f: string, lastName: string } π
You can provide additional options. Available options:
| Option | Type | Default | Description |
|---|---|---|---|
fill | boolean | true | Whether to include defaults in the resulting schema or not (links are NOT transpiled at the moment). |
transform | boolean | true | Whether to include post-validation transformations (i.e. savedAs and transform) in the resulting schema or not. |
mode | put or key | put | Wether to keep only key attributes in the schema or not (update mode is NOT supported at the moment). |
defined | boolean | false | Whether to reject undefined values in the resulting schema or not (even if the original schema is optional). |
- Not Filled
- Not Transformed
- Key
- Defined
const schema = item({
pokemonId: string(),
level: number().default(1)
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.parser({ fill: false })
// β `level` missing
zodSchema.parse({ pokemonId: 'pikachu1' })
const addOne = {
encode: (input: number) => input + 1,
decode: (saved: number) => saved - 1
}
const schema = item({
pokemonId: string().savedAs('id'),
level: number().transform(addOne)
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.parser({ transform: false })
zodSchema.parse({ pokemonId: 'pikachu1', level: 1 })
// β
=> { pokemonId: 'pikachu1', level: 1 }
const schema = item({
pokemonId: string().key(),
level: number()
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.parser({ mode: 'key' })
zodSchema.parse({ pokemonId: 'pikachu1' })
// β
=> { pokemonId: 'pikachu1' }
const schema = string().optional()
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.parser({ defined: true })
// β Input is NOT optional
zodSchema.parse(undefined)
formatter(...)β
(options?: ZodFormatterOptions) => ZodFormatter<SCHEMA>
Creates a zodSchema to use for Item formatting. It includes transformations, custom validation and formatting (see the Formatter action for more details on formatting):
import { z } from 'zod'
const nameSchema = item({
firstName: string().savedAs('_f'),
lastName: string().validate(input => input.length > 1)
})
const zodFormatter = nameSchema
.build(ZodSchemer)
.formatter()
// β `_f` missing
zodFormatter.parse({ lastName: 'Dow' })
// β `lastName` too short
zodFormatter.parse({ _f: 'John', lastName: '' })
// β
Success
zodFormatter.parse({ _f: 'John', lastName: 'Dow' })
type FormattedName = z.infer<typeof zodFormatter>
// => { firstName: string, lastName: string } π
You can provide additional options. Available options:
| Option | Type | Default | Description |
|---|---|---|---|
transform | boolean | true | Whether to include pre-validation transformations (i.e. savedAs and transform) in the resulting schema or not. |
format | boolean | true | Whether to strip hidden attributes from the resulting schema or not. |
partial | boolean | put | Wether to make every attribute (flat or deep) optional in the schema or not. |
defined | boolean | false | Whether to reject undefined values in the resulting schema or not (even if the original schema is optional). |
- Not Transformed
- Not Formatted
- Partial
- Defined
const schema = item({
pokemonId: string().savedAs('id'),
level: number().transform(input => input + 1)
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.formatter({ transform: false })
zodSchema.parse({ pokemonId: 'pikachu1', level: 1 })
// β
=> { pokemonId: 'pikachu1', level: 1 }
const schema = item({
pokemonId: string(),
createdAt: string().hidden()
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.formatter({ format: false })
// β `createdAt` missing
zodSchema.parse({ pokemonId: 'pikachu1' })
const schema = item({
pokemonId: string(),
level: number()
})
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.formatter({ partial: true })
zodSchema.parse({})
// β
=> {}
const schema = string().optional()
const zodSchemer = schema.build(ZodSchemer)
const zodSchema = zodSchemer.formatter({ defined: true })
// β Input is NOT optional
zodSchema.parse(undefined)