Skip to content

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.

ts
// 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'] }

View on GitHub


2. Using with fetch

Fetch a schema from a REST endpoint and run validation on form submission.

ts
// 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)
})
ts
// 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.

ts
// 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)
})
ts
// 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)
})
ts
// 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.

ts
// 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: ['日本の電話番号形式で入力してください'] }

View on GitHub


5. Chaining with the Result Type

Wrap the result of validateObject in the Result type and chain success/failure paths with flatMap.

ts
// 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'] }

View on GitHub