Structures
Structural validators orchestrate nested validation pipelines and maintain proper issue paths for complex data shapes.
object(shape, message?)
Validates an object with specified properties. Unknown keys are allowed by default (see strictObject() for strict mode).
Parameters:
shape: Object mapping keys to schemas- Properties can be wrapped in
[]to make them optional message: Custom error message or handler
Issue Codes:
'object:expected_object': Value is not an object or is an array/null- Plus any issues from nested property validators
import { InferOutput } from '@valchecker/internal'
const user = v.object({
id: v.string(),
name: v.string()
.toTrimmed(),
age: [v.number()
.min(0)], // Optional
})
user.execute({ id: '123', name: ' Alice ' })
// { value: { id: '123', name: 'Alice', age: undefined } }
user.execute({ id: '123', name: ' Alice ', extra: 'ignored' })
// { value: { id: '123', name: 'Alice', age: undefined } }
// Note: 'extra' is stripped from outputType Inference
import { InferOutput } from '@valchecker/internal'
type User = InferOutput<typeof user>
// { id: string; name: string; age: number | undefined }strictObject(shape, message?)
Like object() but rejects unknown keys.
Issue Codes:
'object:expected_object': Value is not an object'object:unknown_key': Object contains keys not in shape
import { InferOutput } from '@valchecker/internal'
const strict = v.strictObject({
id: v.string(),
})
strict.execute({ id: '123', extra: 'not allowed' })
// { issues: [{ code: 'object:unknown_key', ... }] }
strict.execute({ id: '123' })
// { value: { id: '123' } }looseObject(shape, message?)
Alias for object(). Explicitly allows unknown keys while validating declared properties.
array(elementSchema, message?)
Validates each element of an array with the provided schema.
Issue Codes:
'array:expected_array': Value is not an array- Plus any issues from element validators (with index in path)
import { InferOutput } from '@valchecker/internal'
const tags = v.array(v.string()
.toLowercase())
.min(1)
.max(5)
tags.execute(['JS', 'TS', 'NODE'])
// { value: ['js', 'ts', 'node'] }
tags.execute(['a', 123, 'c'])
// { issues: [{ path: [1], code: 'string:expected_string', ... }] }
tags.execute([])
// { issues: [{ code: 'min:expected_min', ... }] }Chainable Methods:
min(count)- Minimum array lengthmax(count)- Maximum array lengthtoFiltered(predicate)- Filter elementstoSorted(compareFn?)- Sort arraytoSliced(start, end?)- Slice arraytoLength()- Replace array with its length (number)
union(schemas, message?)
Tries each schema in order, returns the first success. Fails only if all schemas fail.
Issue Code: Union itself doesn't produce issues; you see issues from branches if all fail
import { InferOutput } from '@valchecker/internal'
const id = v.union([
v.string()
.toTrimmed(),
v.number()
.integer()
.min(0),
])
id.execute('abc') // { value: 'abc' }
id.execute(123) // { value: 123 }
id.execute(true) // { issues: [from first branch, from second branch] }
type ID = InferOutput<typeof id>
// string | numberDiscriminated Unions
For objects with a discriminator field:
import { InferOutput } from '@valchecker/internal'
const event = v.union([
v.object({
type: v.literal('click'),
x: v.number(),
y: v.number(),
}),
v.object({
type: v.literal('keypress'),
key: v.string(),
}),
])
type Event = InferOutput<typeof event>
// | { type: 'click'; x: number; y: number }
// | { type: 'keypress'; key: string }intersection(schemas, message?)
Runs all schemas and merges their results. All schemas must pass.
import { InferOutput } from '@valchecker/internal'
const timestamped = v.object({
createdAt: v.number(),
updatedAt: v.number(),
})
const auditable = v.object({
createdBy: v.string(),
updatedBy: v.string(),
})
const entity = v.intersection([timestamped, auditable])
entity.execute({
createdAt: 1234567890,
updatedAt: 1234567890,
createdBy: 'alice',
updatedBy: 'bob',
})
// { value: { createdAt: ..., updatedAt: ..., createdBy: ..., updatedBy: ... } }
type Entity = InferOutput<typeof entity>
// { createdAt: number; updatedAt: number; createdBy: string; updatedBy: string }instance(constructor, message?)
Validates that a value is an instance of the given constructor.
Issue Code: 'instance:expected_instance'
import { InferOutput } from '@valchecker/internal'
const dateSchema = v.instance(Date)
dateSchema.execute(new Date()) // { value: Date }
dateSchema.execute('2024-01-01') // { issues: [...] }
// Custom classes
class User {
constructor(public name: string) {}
}
const userInstance = v.instance(User)
userInstance.execute(new User('Alice')) // { value: User { name: 'Alice' } }
// Built-in types
const regexSchema = v.instance(RegExp)
const errorSchema = v.instance(Error)
const mapSchema = v.instance(Map)Nested Issue Paths
Structural validators automatically prepend keys or indexes to issue paths:
import { InferOutput } from '@valchecker/internal'
const schema = v.object({
users: v.array(
v.object({
profile: v.object({
name: v.string(),
}),
}),
),
})
schema.execute({
users: [
{ profile: { name: 'Alice' } },
{ profile: { name: 123 } }, // ← Invalid
],
})
// issues[0].path === ['users', 1, 'profile', 'name']This makes it easy to:
- Highlight exact failing fields in forms
- Map errors to UI components
- Generate human-readable error locations
Combining Structures
Structures can be freely nested and combined:
import { InferOutput } from '@valchecker/internal'
const addressSchema = v.object({
street: v.string(),
city: v.string(),
zip: v.string(),
})
const companySchema = v.object({
name: v.string(),
addresses: v.array(addressSchema)
.min(1),
contacts: v.object({
primary: v.string(),
backup: [v.string()],
}),
})
const orderSchema = v.object({
id: v.string(),
company: companySchema,
items: v.array(
v.object({
productId: v.string(),
quantity: v.number()
.integer()
.min(1),
price: v.number()
.min(0),
})
),
shippingAddress: addressSchema,
billingAddress: [addressSchema], // Optional
})