exodus/schemasafe
Form validator implementation based on @exodus/schemasafe a code-generating JSON Schema validator.
Installation
Section titled “Installation”npm i @sjsf/schemasafe-validator@next ajv@8
yarn add @sjsf/schemasafe-validator@next ajv@8
pnpm add @sjsf/schemasafe-validator@next ajv@8
bun add @sjsf/schemasafe-validator@next ajv@8
Example
Section titled “Example”<script lang="ts"> import { ON_INPUT, BasicForm } from "@sjsf/form"; import { createFormValidator } from "@sjsf/schemasafe-validator";
import { createMyForm } from "@/components/my-form";
import { initialValue, schema, uiSchema } from "../shared";
const validator = createFormValidator({ uiSchema, });
const form = createMyForm({ schema, uiSchema, validator, fieldsValidationMode: ON_INPUT, initialValue, });</script>
<BasicForm {form} novalidate />
<pre>{JSON.stringify(form.value, null, 2)}</pre>
import type { Schema, UiSchema } from "@sjsf/form";
export const schema: Schema = { type: "object", properties: { id: { type: "string", minLength: 8, pattern: "\\d+", }, active: { type: "boolean", }, skills: { type: "array", minItems: 4, items: { type: "string", minLength: 5, }, }, multipleChoicesList: { type: "array", maxItems: 2, items: { type: "string", enum: ["foo", "bar", "fuzz"], }, }, },};
export const uiSchema: UiSchema = { id: { "ui:options": { title: "Identifier", }, }, active: { "ui:options": { title: "Active", }, }, multipleChoicesList: { "ui:options": { title: "Pick max two items", }, },};
export const initialValue = { id: "Invalid", skills: ["karate", "budo", "aikido"], multipleChoicesList: ["foo", "bar", "fuzz"],};
Precompiled validation
Section titled “Precompiled validation”It is possible to use precompiled validator.
Schema precompilation
Section titled “Schema precompilation”The first step in the process is to compile a schema into a set of validate functions.
import fs from "node:fs";import path from "node:path";
import { validator } from '@exodus/schemasafe'
import { ON_INPUT } from '@sjsf/form';import { insertSubSchemaIds, fragmentSchema,} from "@sjsf/form/validators/precompile";import { DEFAULT_VALIDATOR_OPTIONS, FORM_FORMATS } from '@sjsf/schemasafe-validator'
import inputSchema from './input-schema.json' with { type: "json" }
const fieldsValidationMode = ON_INPUT
// NOTE: After calling this function, be sure to save the `schema` and// use it to generate the formconst patch = insertSubSchemaIds(inputSchema as any, { fieldsValidationMode });
// It is easier to save as a TS file// https://github.com/microsoft/TypeScript/issues/32063fs.writeFileSync( path.join(import.meta.dirname, "patched-schema.ts"), `import type { Schema } from "@sjsf/form";export const fieldsValidationMode = ${fieldsValidationMode}export const schema = ${JSON.stringify(patch.schema, null, 2)} as const satisfies Schema;`);
const schemas = fragmentSchema(patch)
// @ts-expect-error No typing for `multi` version of functionconst validate = validator(schemas, { ...DEFAULT_VALIDATOR_OPTIONS, formats: { ...FORM_FORMATS, "phone-us": /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, "area-code": /\d{3}/, }, schemas: new Map(schemas.map(schema => [schema.$id, schema])), multi: true,})
const validateFunctions = `export const [${schemas.map(s => s.$id).join(', ')}] = ${validate.toModule()}`
fs.writeFileSync(path.join(import.meta.dirname, "validate-functions.js"), validateFunctions)
{ "$id": "root", "definitions": { "test": { "type": "string" }, "foo": { "type": "object", "properties": { "name": { "type": "string" } } }, "price": { "title": "Price per task ($)", "type": "number", "multipleOf": 0.03, "minimum": 1 }, "passwords": { "type": "object", "properties": { "pass1": { "type": "string" }, "pass2": { "type": "string" } }, "required": ["pass1", "pass2"] }, "list": { "type": "array", "items": { "type": "string" } }, "choice1": { "type": "object", "properties": { "choice": { "type": "string", "const": "one" }, "other": { "type": "number" } } }, "choice2": { "type": "object", "properties": { "choice": { "type": "string", "const": "two" }, "more": { "type": "string" } } } }, "type": "object", "properties": { "foo": { "type": "string" }, "price": { "$ref": "#/definitions/price" }, "passwords": { "$ref": "#/definitions/passwords" }, "dataUrlWithName": { "type": "string", "format": "data-url" }, "phone": { "type": "string", "format": "phone-us" }, "multi": { "anyOf": [{ "$ref": "#/definitions/foo" }] }, "list": { "$ref": "#/definitions/list" }, "single": { "oneOf": [ { "$ref": "#/definitions/choice1" }, { "$ref": "#/definitions/choice2" } ] }, "anything": { "type": "object", "additionalProperties": { "type": "string" } } }, "anyOf": [ { "title": "First method of identification", "properties": { "firstName": { "type": "string", "title": "First name" }, "lastName": { "$ref": "#/definitions/test" } } }, { "title": "Second method of identification", "properties": { "idCode": { "$ref": "#/definitions/test" } } } ]}
node --experimental-strip-types compile-schema-script.ts
<script lang="ts"> import { BasicForm } from "@sjsf/form"; import { createFormValidator } from "@sjsf/schemasafe-validator/precompile";
import { createMyForm } from "@/components/my-form";
import { schema, fieldsValidationMode } from "./patched-schema"; import * as validateFunctions from "./validate-functions";
const validator = createFormValidator({ validateFunctions });
const form = createMyForm({ schema, validator, fieldsValidationMode, });</script>
<BasicForm {form} novalidate />
<pre>{JSON.stringify(form.value, null, 2)}</pre>
import type { Schema } from "@sjsf/form";export const fieldsValidationMode = 1export const schema = { "$id": "v0", "definitions": { "test": { "type": "string", "$id": "v1" }, "foo": { "type": "object", "properties": { "name": { "type": "string", "$id": "v2" } }, "$id": "v14" }, "price": { "title": "Price per task ($)", "type": "number", "multipleOf": 0.03, "minimum": 1, "$id": "v3" }, "passwords": { "type": "object", "properties": { "pass1": { "type": "string", "$id": "v4" }, "pass2": { "type": "string", "$id": "v5" } }, "required": [ "pass1", "pass2" ] }, "list": { "type": "array", "items": { "type": "string", "$id": "v6" } }, "choice1": { "type": "object", "properties": { "choice": { "type": "string", "const": "one", "$id": "v7" }, "other": { "type": "number", "$id": "v8" } }, "$id": "v15" }, "choice2": { "type": "object", "properties": { "choice": { "type": "string", "const": "two", "$id": "v9" }, "more": { "type": "string", "$id": "v10" } }, "$id": "v16" } }, "type": "object", "properties": { "foo": { "type": "string", "$id": "v11" }, "price": { "$ref": "#/definitions/price" }, "passwords": { "$ref": "#/definitions/passwords" }, "dataUrlWithName": { "type": "string", "format": "data-url", "$id": "v12" }, "phone": { "type": "string", "format": "phone-us", "$id": "v13" }, "multi": { "anyOf": [ { "$ref": "#/definitions/foo" } ] }, "list": { "$ref": "#/definitions/list" }, "single": { "oneOf": [ { "$ref": "#/definitions/choice1" }, { "$ref": "#/definitions/choice2" } ] }, "anything": { "type": "object", "additionalProperties": { "type": "string", "$id": "v17" } } }, "anyOf": [ { "title": "First method of identification", "properties": { "firstName": { "type": "string", "title": "First name", "$id": "v19" }, "lastName": { "$ref": "#/definitions/test" } }, "$id": "v18" }, { "title": "Second method of identification", "properties": { "idCode": { "$ref": "#/definitions/test" } }, "$id": "v20" } ]} as const satisfies Schema;