Custom Format Validators in z-schema
JSON Schema
format
constrains string (or other) values beyond basic type checking. z-schema includes built-in validators and supports registering custom ones — both sync and async.
Built-in formats
z-schema ships with validators for all standard JSON Schema formats:
Format
Validates
date
RFC 3339 full-date (
2024-01-15
)
date-time
RFC 3339 date-time (
2024-01-15T09:30:00Z
)
time
RFC 3339 time (
09:30:00Z
)
email
RFC 5321 email address
idn-email
Internationalized email
hostname
RFC 1123 hostname
idn-hostname
Internationalized hostname
ipv4
IPv4 address
ipv6
IPv6 address
uri
RFC 3986 URI
uri-reference
URI or relative reference
uri-template
RFC 6570 URI template
iri
Internationalized URI
iri-reference
Internationalized URI reference
json-pointer
RFC 6901 JSON Pointer
relative-json-pointer
Relative JSON Pointer
regex
ECMA-262 regex
duration
ISO 8601 duration
uuid
RFC 4122 UUID
Registering a sync format
A format validator is a function
(input: unknown) => boolean
. Return
true
if valid,
false
if invalid. Return
true
for non-applicable types (e.g., when input is not a string) — z-schema calls format validators for any value type.
Global registration (shared across all instances)
import
ZSchema
from
'z-schema'
;
ZSchema
.
registerFormat
(
'postal-code'
,
(
value
)
=>
{
if
(
typeof
value
!==
'string'
)
return
true
;
// skip non-strings
return
/
^
\d
{5}
(
-
\d
{4}
)
?
$
/
.
test
(
value
)
;
}
)
;
Instance-scoped registration
const
validator
=
ZSchema
.
create
(
)
;
validator
.
registerFormat
(
'postal-code'
,
(
value
)
=>
{
if
(
typeof
value
!==
'string'
)
return
true
;
return
/
^
\d
{5}
(
-
\d
{4}
)
?
$
/
.
test
(
value
)
;
}
)
;
Instance formats override global formats with the same name.
Via options at creation time
const
validator
=
ZSchema
.
create
(
{
customFormats
:
{
'postal-code'
:
(
value
)
=>
typeof
value
===
'string'
&&
/
^
\d
{5}
(
-
\d
{4}
)
?
$
/
.
test
(
value
)
,
'always-valid'
:
(
)
=>
true
,
'disable-email'
:
null
,
// disable the built-in email validator
}
,
}
)
;
Pass
null
to disable a built-in or globally registered format.
Registering an async format
Return
Promise
. The validator must be created with
{ async: true }
.
const
validator
=
ZSchema
.
create
(
{
async
:
true
}
)
;
validator
.
registerFormat
(
'user-exists'
,
async
(
value
)
=>
{
if
(
typeof
value
!==
'number'
)
return
true
;
const
user
=
await
db
.
findUser
(
value
)
;
return
user
!=
null
;
}
)
;
// Validate (returns Promise)
try
{
await
validator
.
validate
(
data
,
schema
)
;
}
catch
(
err
)
{
console
.
log
(
err
.
details
)
;
}
Timeout
Async format validators time out after
asyncTimeout
milliseconds (default: 2000). Increase for slow operations:
const
validator
=
ZSchema
.
create
(
{
async
:
true
,
asyncTimeout
:
10000
,
// 10 seconds
}
)
;
Timed-out validators produce an
ASYNC_TIMEOUT
error.
Format assertion behavior across drafts
The JSON Schema spec changed how
format
works in newer drafts:
Draft
Default behavior
With
formatAssertions: true
draft-04/06/07
Always asserts (fails on mismatch)
Always asserts
draft-2019-09
Always asserts (z-schema default)
Annotation-only unless vocabulary enabled
draft-2020-12
Always asserts (z-schema default)
Annotation-only unless vocabulary enabled
z-schema defaults to
formatAssertions: null
(legacy — always assert). To respect the spec's vocabulary-aware behavior for modern drafts:
const
validator
=
ZSchema
.
create
(
{
formatAssertions
:
true
}
)
;
To disable all format assertions (annotation-only):
const
validator
=
ZSchema
.
create
(
{
formatAssertions
:
false
}
)
;
Unknown formats
By default, z-schema reports
UNKNOWN_FORMAT
for unrecognized format names in draft-04/06/07. Modern drafts (2019-09, 2020-12) always silently ignore unknown formats.
To suppress unknown format errors in older drafts:
const
validator
=
ZSchema
.
create
(
{
ignoreUnknownFormats
:
true
}
)
;
Unregistering a format
// Global
ZSchema
.
unregisterFormat
(
'postal-code'
)
;
// Instance
validator
.
unregisterFormat
(
'postal-code'
)
;
Listing formats
// List globally registered custom formats
const
customFormats
=
ZSchema
.
getRegisteredFormats
(
)
;
// List all supported formats (built-in + custom) on an instance
const
allFormats
=
validator
.
getSupportedFormats
(
)
;
// Check if a specific format is supported
const
supported
=
validator
.
isFormatSupported
(
'postal-code'
)
;
Real-world patterns
Phone number validation
ZSchema
.
registerFormat
(
'phone'
,
(
value
)
=>
{
if
(
typeof
value
!==
'string'
)
return
true
;
return
/
^
+
?
[
1
-
9
]
\d
{1,14}
$
/
.
test
(
value
)
;
// E.164 format
}
)
;
ISO 8601 date (strict)
ZSchema
.
registerFormat
(
'iso-date'
,
(
value
)
=>
{
if
(
typeof
value
!==
'string'
)
return
true
;
const
d
=
new
Date
(
value
)
;
return
!
isNaN
(
d
.
getTime
(
)
)
&&
value
===
d
.
toISOString
(
)
.
split
(
'T'
)
[
0
]
;
}
)
;
Business rule: value from external list
const
validator
=
ZSchema
.
create
(
{
async
:
true
}
)
;
validator
.
registerFormat
(
'valid-country'
,
async
(
value
)
=>
{
if
(
typeof
value
!==
'string'
)
return
true
;
const
countries
=
await
fetchValidCountries
(
)
;
return
countries
.
includes
(
value
.
toUpperCase
(
)
)
;
}
)
;
Side-effect: prefill defaults
Format validators can mutate objects (use with caution):
ZSchema
.
registerFormat
(
'fill-defaults'
,
(
obj
)
=>
{
if
(
typeof
obj
===
'object'
&&
obj
!==
null
)
{
(
obj
as
Record
<
string
,
unknown
)
.
createdAt
??=
new
Date
(
)
.
toISOString
(
)
;
}
return
true
;
}
)
;
Schema usage
{
"type"
:
"object"
,
"properties"
:
{
"phone"
:
{
"type"
:
"string"
,
"format"
:
"phone"
}
,
"country"
:
{
"type"
:
"string"
,
"format"
:
"valid-country"
}
,
"zipCode"
:
{
"type"
:
"string"
,
"format"
:
"postal-code"
}
}
}