Custom Schema

Writing your own schemas and adapting other validation libraries to kvant.

Any object with parse and encode functions is a valid kvant schema:

interface KvantSchema<Output, Input = Output, RawInput = unknown> {
  parse: (value: RawInput) => Output  // storage to state
  encode: (value: Output) => Input    // state to storage
}

Rolling your own

The simplest schema is a plain object:

import type {  } from 'kvantjs'
import {  } from 'kvantjs/vue'

const : <string[] | undefined, string | undefined> = {
  :  =>  ? ().(',').() : ,
  :  => ?. ? .(',') : ,
}

.('a,b,c') // ['a', 'b', 'c']
.(['a', 'b']) // 'a,b'
.([]) // undefined (removes the key)

const  = ('tags', )

You can also use kv.custom() from kvantjs/schema, which gives you the same result with full type inference and access to the chainable combinators (.default(), .pipe(), .refine(), ...):

import * as  from 'kvantjs/schema'
import {  } from 'kvantjs/vue'

const  = .<string[] | undefined, string | undefined>({
  :  =>  ? ().(',').() : ,
  :  => ?. ? .(',') : ,
})

const  = ('tags', .([]))

Contract, in short:

  1. Never throw. Return undefined for invalid input instead. URLs, cookies, and storage are user-editable, and throwing turns a bad value into a broken page.
  2. Lossless. encode(parse(v)) preserves valid values.
  3. Pure. No side effects: same input, same output.

Adapting other schema libraries

Valibot

Valibot is validation-only, so wrap it with an encode side and a non-throwing parse:

import * as v from 'valibot'
import type { KvantSchema } from 'kvantjs'

function valibot<T>(
  schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>
): KvantSchema<T | undefined> {
  return {
    parse: (value) => {
      const result = v.safeParse(schema, value)
      return result.success ? result.output : undefined
    },
    encode: (value) => value,
  }
}

ArkType

ArkType schemas return errors instead of throwing, so the adapter is straightforward:

import { type Type, ArkErrors } from 'arktype'
import type { KvantSchema } from 'kvantjs'

function arktype<T>(
  def: Type<T>
): KvantSchema<T | undefined> {
  return {
    parse: (value) => {
      const out = def(value)
      return out instanceof ArkErrors ? undefined : out as T
    },
    encode: (value) => value,
  }
}

Standard Schema

Any Standard Schema-compatible library works through the same adapter shape:

import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { KvantSchema } from 'kvantjs'

function standard<T>(
  schema: StandardSchemaV1<unknown, T>
): KvantSchema<T | undefined> {
  return {
    parse: (value) => {
      const result = schema['~standard'].validate(value)
      if (result instanceof Promise)
        throw new TypeError('Async schemas are not supported by kvant')
      return result.issues ? undefined : result.value
    },
    encode: (value) => value,
  }
}

The pattern is always the same: safe-parse on the way in, identity (or a codec) on the way out, and undefined, never an exception, for invalid input.

On this page