How to add custom validation#
Goal: enforce a rule that the built-in builders don't cover—a regular expression, or arbitrary logic.
Regular expressions#
Pass a RegExp (TS) or *regexp.Regexp (Go) to Check.
TS
const { Shape, Check } = require('shape')
const shape = Shape({ email: Check(/^.+@.+$/) })
shape({ email: 'a@b.co' }) // OK
shape({ email: 'nope' }) // throws: check "/^.+@.+$/" failed
Go
import "regexp"
s := shape.MustShape(map[string]any{
"email": shape.Check(regexp.MustCompile(`^.+@.+$`)),
})
A pattern is held to the shared regexp subset: what the JavaScript, RE2 and regex-crate engines all read the same way, with
\d,\wand\sASCII and no flags, lookaround or backreferences. Inside it a shape behaves identically in every language.
A custom predicate#
Pass a function to Check. Return true to pass. You may also read/replace the
value and set a custom error.
TS
const shape = Shape({
even: Check((val, update, state) => {
if (typeof val !== 'number' || val % 2 !== 0) {
update.err = 'must be an even number'
return false
}
return true
}),
})
Go
s := shape.MustShape(map[string]any{
"even": shape.Check(func(val any, u *shape.Update, s *shape.State) bool {
n, ok := val.(float64)
if !ok || int(n)%2 != 0 {
u.Err = "must be an even number"
return false
}
return true
}),
})
The validator signature is the same in both languages:
(val, update, state) => bool. See
Shape nodes for the update/state fields.
Before and after the structural checks#
Beforeruns before the type check: use it to coerce or substitute a value.Afterruns after: use it to validate the produced value.
Shape({
id: Before((val, update) => (undefined !== val && (update.val = String(val)), true), String),
})
A Before also runs when the property is absent (val is undefined), so
leave an absent value alone or the required check never runs—without the
preceding guard, a missing id would become the string 'undefined'.
Coerce, repair or rewrite the value#
For the common cases there is no need to write a validator:
Coerceconverts"8080"to8080(and the other unambiguous conversions) before the type check.Catchreplaces whatever fails with a fallback.Transformreplaces a valid value with a function of it.- The string formats (
Email,Url,Uuid,DateTime,Ip) check the usual formats portably.
Replace the message#
Wrap any shape in Fault to override the
structural error text (custom-check messages are controlled by the check
itself):
Shape({ port: Fault('port must be a number', Number) })
Do not use an async function#
Validation is synchronous by design (see
ADR 0001). An async validator
returns a promise, a promise is truthy, and a truthy return means "passed"—so
the check silently succeeds no matter what it would have decided:
const check = async (v) => v > 100 // would reject 5
Shape(Check(check, Number))(5) // → 5. Passes.
Validate the shape first, then apply rules that need I/O to the produced value:
const user = Shape({ email: String, name: String })(input)
if (await emailTaken(user.email)) {
throw new Error('email already registered')
}
See also#
- ADR 0001—validation is synchronous.
- Compose shapes for
One/Some/All/Exact. - Handle and collect errors.