Examples - Client
Client-side implementation examples using @uuki/schemable-validator-client.
Source code:
packages/client/examples/
1. Basic Usage
Match an object against a schema with validateObject, then retrieve results using isAllValid / extractErrors.
// Basic usage: validate a plain object against a schema.
//
// The schema is a JSON Schema object — typically received from a REST endpoint
// (e.g. GET /wp-json/schv/v1/contact), but here inlined for illustration.
import { validateObject, isAllValid, extractErrors } from '@uuki/schemable-validator-client'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
// Example schema
const schema: ObjectSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
body: { type: 'string', minLength: 10 },
},
required: ['name', 'email', 'body'],
}
// --- Valid input ---
const valid = validateObject(
{ name: 'Alice', email: 'alice@example.com', body: 'お問い合わせ内容です。' },
schema,
)
console.log(isAllValid(valid)) // true
// --- Invalid input ---
const invalid = validateObject(
{ name: '', email: 'not-an-email', body: '' },
schema,
)
console.log(isAllValid(invalid)) // false
console.log(extractErrors(invalid))
// {
// name: ['is required'],
// email: ['must be a valid email'],
// body: ['is required'],
// }
// --- Per-field result shape (mirrors PHP getResult()) ---
console.log(invalid.email)
// { value: 'not-an-email', is_valid: false, errors: ['must be a valid email'] }2. Using with fetch
Fetch a schema from a REST endpoint and run validation on form submission.
// Combining the client library with a fetch call.
//
// The client library has no opinion on transport. Here we use the browser's fetch API,
// but any HTTP client (axios, ky, wretch, …) works the same way.
import { validateObject, isAllValid, extractErrors } from '@uuki/schemable-validator-client'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
// Fetch the schema once, cache it yourself — the client library does not own this concern.
async function fetchSchema(url: string): Promise<ObjectSchema> {
const res = await fetch(url, { headers: { Accept: 'application/json' } })
if (!res.ok) throw new Error(`schema fetch failed: ${res.status}`)
return res.json() as Promise<ObjectSchema>
}
// Example: validate on form submit
async function handleSubmit(form: HTMLFormElement): Promise<void> {
// Any endpoint that returns a JSON Schema object
const schema = await fetchSchema('/api/schema/contact')
// FormData → plain object (File entries are excluded; handled server-side)
const data = Object.fromEntries(
[...new FormData(form).entries()].flatMap(([k, v]) =>
typeof v === 'string' ? [[k, v]] : [],
),
) as Record<string, string>
const result = validateObject(data, schema)
if (isAllValid(result)) {
form.submit()
return
}
for (const [field, errors] of Object.entries(extractErrors(result))) {
console.error(`${field}: ${errors.join(', ')}`)
}
}
// Wire up
document.querySelector('form')?.addEventListener('submit', (e) => {
e.preventDefault()
handleSubmit(e.currentTarget as HTMLFormElement)
})// Combining the client library with a fetch call.
//
// The client library has no opinion on transport. Here we use the browser's fetch API,
// but any HTTP client (axios, ky, wretch, …) works the same way.
import { validateObject, isAllValid, extractErrors } from '@uuki/schemable-validator-client'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
// Fetch the schema once, cache it yourself — the client library does not own this concern.
async function fetchSchema(url: string): Promise<ObjectSchema> {
const res = await fetch(url, { headers: { Accept: 'application/json' } })
if (!res.ok) throw new Error(`schema fetch failed: ${res.status}`)
return res.json() as Promise<ObjectSchema>
}
// Example: validate on form submit
async function handleSubmit(form: HTMLFormElement): Promise<void> {
// PHP helper schv_schema_url('/contact') returns this URL
const schema = await fetchSchema('/wp-json/schv/v1/contact')
// FormData → plain object (File entries are excluded; handled server-side)
const data = Object.fromEntries(
[...new FormData(form).entries()].flatMap(([k, v]) =>
typeof v === 'string' ? [[k, v]] : [],
),
) as Record<string, string>
const result = validateObject(data, schema)
if (isAllValid(result)) {
form.submit()
return
}
for (const [field, errors] of Object.entries(extractErrors(result))) {
console.error(`${field}: ${errors.join(', ')}`)
}
}
// Wire up
document.querySelector('form')?.addEventListener('submit', (e) => {
e.preventDefault()
handleSubmit(e.currentTarget as HTMLFormElement)
})3. Integrating with Framework Validators
Fetch the server-defined JSON Schema and inject it into Zod, Valibot, or AJV. Client-side rules stay in sync with the PHP definition automatically.
Zod and Valibot use the built-in adapters; AJV accepts the JSON Schema output directly.
// Validating with AJV using the JSON Schema output directly.
//
// AJV consumes the ObjectSchema as-is — no adapter needed.
// Custom keywords (x-when, x-unmapped-fields) are passed through
// but ignored by AJV unless you register them explicitly.
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
async function fetchSchema(url: string): Promise<ObjectSchema> {
const res = await fetch(url, { headers: { Accept: 'application/json' } })
if (!res.ok) throw new Error(`schema fetch failed: ${res.status}`)
return res.json() as Promise<ObjectSchema>
}
async function handleSubmit(form: HTMLFormElement): Promise<void> {
const jsonSchema = await fetchSchema('/api/schema/contact')
const validate = ajv.compile(jsonSchema)
const data = Object.fromEntries(
[...new FormData(form).entries()].flatMap(([k, v]) =>
typeof v === 'string' ? [[k, v]] : [],
),
)
const valid = validate(data)
if (valid) {
form.submit()
return
}
console.error(validate.errors)
}
document.querySelector('form')?.addEventListener('submit', (e) => {
e.preventDefault()
handleSubmit(e.currentTarget as HTMLFormElement)
})// Injecting a JSON Schema response into Zod via the built-in adapter.
//
// toZodSchema converts the server-defined ObjectSchema to a z.ZodObject,
// keeping client-side rules automatically in sync with the PHP definition.
import { toZodSchema } from '@uuki/schemable-validator-client/zod'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
async function fetchSchema(url: string): Promise<ObjectSchema> {
const res = await fetch(url, { headers: { Accept: 'application/json' } })
if (!res.ok) throw new Error(`schema fetch failed: ${res.status}`)
return res.json() as Promise<ObjectSchema>
}
async function handleSubmit(form: HTMLFormElement): Promise<void> {
const jsonSchema = await fetchSchema('/api/schema/contact')
const schema = toZodSchema(jsonSchema)
const data = Object.fromEntries(
[...new FormData(form).entries()].flatMap(([k, v]) =>
typeof v === 'string' ? [[k, v]] : [],
),
)
const result = schema.safeParse(data)
if (result.success) {
form.submit()
return
}
console.error(result.error.flatten().fieldErrors)
}
document.querySelector('form')?.addEventListener('submit', (e) => {
e.preventDefault()
handleSubmit(e.currentTarget as HTMLFormElement)
})// Injecting a JSON Schema response into Valibot (v1.x) via the built-in adapter.
//
// toValibotSchema converts the server-defined ObjectSchema to a v.ObjectSchema,
// keeping client-side rules automatically in sync with the PHP definition.
import * as v from 'valibot'
import { toValibotSchema } from '@uuki/schemable-validator-client/valibot'
import type { ObjectSchema } from '@uuki/schemable-validator-client'
async function fetchSchema(url: string): Promise<ObjectSchema> {
const res = await fetch(url, { headers: { Accept: 'application/json' } })
if (!res.ok) throw new Error(`schema fetch failed: ${res.status}`)
return res.json() as Promise<ObjectSchema>
}
async function handleSubmit(form: HTMLFormElement): Promise<void> {
const jsonSchema = await fetchSchema('/api/schema/contact')
const schema = toValibotSchema(jsonSchema)
const data = Object.fromEntries(
[...new FormData(form).entries()].flatMap(([k, vs]) =>
typeof vs === 'string' ? [[k, vs]] : [],
),
)
const result = v.safeParse(schema, data)
if (result.success) {
form.submit()
return
}
console.error(v.flatten(result.issues).nested)
}
document.querySelector('form')?.addEventListener('submit', (e) => {
e.preventDefault()
handleSubmit(e.currentTarget as HTMLFormElement)
})4. Adding a Custom Constraint
Define a Constraint (a pure function of FieldState → FieldState) and compose it with the built-in validation as additional verification.
// Adding a custom Constraint to the pipeline.
//
// A Constraint is a pure function: FieldState → FieldState.
// It appends to the errors array when a check fails, and passes the state
// through unchanged when the check passes. Multiple constraints compose
// with composeConstraints — all errors are accumulated, not short-circuited.
import { composeConstraints, constraintsFromSchema, validateObject } from '@uuki/schemable-validator-client'
import type { Constraint, ObjectSchema } from '@uuki/schemable-validator-client'
// --- Define a custom constraint ---
// Japanese phone number (hyphenated or 10/11-digit flat)
const checkJapanesePhone: Constraint = (state) =>
/^(0\d{9,10}|0\d{1,4}-\d{1,4}-\d{3,4})$/.test(state.value)
? state
: { ...state, errors: [...state.errors, '日本の電話番号形式で入力してください'] }
// Compose: built-in string type check → custom phone format check
const phoneConstraint = composeConstraints([
constraintsFromSchema({ type: 'string' }),
checkJapanesePhone,
])
console.log(phoneConstraint({ value: '09012345678', errors: [] }).errors) // []
console.log(phoneConstraint({ value: '090-1234-5678', errors: [] }).errors) // []
console.log(phoneConstraint({ value: 'invalid', errors: [] }).errors)
// ['日本の電話番号形式で入力してください']
// --- Using it with validateObject ---
//
// validateObject operates on the JSON Schema from the REST endpoint.
// Custom constraints that have no JSON Schema keyword (e.g. phone format)
// are applied after fetching the schema, by wrapping validateObject.
const schema: ObjectSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
phone: { type: 'string' }, // schema only ensures it's a string
},
required: ['name', 'phone'],
}
function validateWithCustomRules(data: Record<string, string>) {
// 1. Run schema-derived constraints
const base = validateObject(data, schema)
// 2. Apply the custom phone constraint on top
const phoneState = phoneConstraint({ value: data.phone ?? '', errors: [] })
return {
...base,
phone: {
value: data.phone ?? '',
is_valid: base.phone.is_valid && phoneState.errors.length === 0,
errors:
[...(base.phone.errors ?? []), ...phoneState.errors].length > 0
? [...(base.phone.errors ?? []), ...phoneState.errors]
: null,
},
}
}
console.log(validateWithCustomRules({ name: 'Alice', phone: 'invalid' }).phone)
// { value: 'invalid', is_valid: false, errors: ['日本の電話番号形式で入力してください'] }5. Chaining with the Result Type
Wrap the result of validateObject in the Result type and chain success/failure paths with flatMap.
// ROP: wrapping validation in Result and chaining with flatMap.
//
// validateObject returns ValidationResult directly (no Result wrapper).
// This example shows how to lift it into Result when you want to
// thread success/failure through a pipeline without nested if-checks.
import { validateObject, isAllValid, extractErrors, ok, err, flatMap } from '@uuki/schemable-validator-client'
import type { Result, ObjectSchema, ValidationResult } from '@uuki/schemable-validator-client'
type ValidationErrors = Readonly<Record<string, readonly string[]>>
// Lift validateObject into Result: Ok if all valid, Err with field errors otherwise
const validate = (
data: Record<string, string>,
schema: ObjectSchema,
): Result<ValidationResult, ValidationErrors> => {
const result = validateObject(data, schema)
return isAllValid(result) ? ok(result) : err(extractErrors(result))
}
// --- Pipeline ---
const schema: ObjectSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
}
type Payload = { name: string; email: string; sanitized: true }
const result = flatMap(
validate({ name: 'Alice', email: 'alice@example.com' }, schema),
(fields): Result<Payload, ValidationErrors> =>
ok({
name: fields.name.value,
email: fields.email.value,
sanitized: true,
}),
)
if (result._tag === 'Ok') {
console.log('submit', result.value)
// submit { name: 'Alice', email: 'alice@example.com', sanitized: true }
} else {
console.error('errors', result.error)
}
// --- Failure path ---
const failed = validate({ name: '', email: 'bad' }, schema)
// Err({ name: ['is required'], email: ['must be a valid email'] })
const withFallback = flatMap(
failed,
() => err({ _form: ['unexpected: should not reach here'] } as ValidationErrors),
)
console.log(withFallback._tag) // 'Err'
console.log(withFallback._tag === 'Err' && withFallback.error)
// { name: ['is required'], email: ['must be a valid email'] }