Conversion and validation utilities for Forman Schema.
Non-breaking minor release. New surface for working with advanced: true Forman fields:
toJSONSchema(field, options?)still returns a bareJSONSchema7— fully backward-compatible.- Fields marked
advanced: trueare now stamped withx-advanced: trueon the JSON Schema output, and round-trip throughtoFormanSchema(which restoresadvanced: true). - New option
excludeAdvancedFields?: boolean(defaultfalse). Whentrue, advanced sub-fields of a collection are omitted from the schema. - New function
toJSONSchemaAdvanced(field, options?)returns{ schema: JSONSchema7, skippedPaths?: { advanced?: string[] } }. Use it to learn which advanced fields were dropped (e.g. to render a "show advanced" toggle).toJSONSchemadelegates to it internally and returns just.schema.
npm install @makehq/forman-schemaimport { toJSONSchema } from '@makehq/forman-schema';
const formanField = {
type: 'collection',
spec: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'age',
type: 'number',
},
],
};
const jsonSchema = toJSONSchema(formanField);Advanced fields (advanced: true) are included by default and stamped with x-advanced: true. To omit them from the rendered schema, pass { excludeAdvancedFields: true }:
const jsonSchema = toJSONSchema(formanField, { excludeAdvancedFields: true });If you also need to know which advanced fields were dropped (e.g. to render a "show advanced" toggle), use toJSONSchemaAdvanced:
import { toJSONSchemaAdvanced } from '@makehq/forman-schema';
const { schema, skippedPaths } = toJSONSchemaAdvanced(formanField, { excludeAdvancedFields: true });
// skippedPaths?.advanced is an array of dot-notation paths like ['wrapper.field', 'wrapper.arr[].nested']The filter applies to sub-fields of a collection — including nested-by-option fields, array-of-collection items, composite expansions (udtspec, udttype), and cross-domain buffered fields. It does not apply to: the top-level field passed in (always converted), or the item type of an array whose spec is a single primitive field. To hide an entire array or any other top-level structure, mark the parent field as advanced: true.
import { toFormanSchema } from '@makehq/forman-schema';
const jsonSchemaField = {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
},
},
required: ['name'],
};
const formanSchema = toFormanSchema(jsonSchemaField);A json field can carry an explicit schema (a JSON Schema). This lets you author complex parts of a form directly in JSON Schema and mix them with primitive Forman fields:
const formanField = {
type: 'collection',
spec: [
{ name: 'title', type: 'text' },
{
name: 'input',
type: 'json',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
},
},
],
};On conversion, the schema is echoed verbatim into the JSON Schema output (the field's label/help fill in title/description only when the schema omits them). An enumerable x-json marker is added so toFormanSchema can recover the json type; it survives JSON serialization. A json field without a schema renders as a plain object schema ({ type: 'object' }), since a JSON value is most naturally an object.
The library cannot validate a JSON value against an arbitrary JSON Schema on its own — it has no JSON Schema validator built in. Validation of json fields is therefore opt-in: a json value is not validated unless you provide a validateJson callback. Without it, the value passes through untouched.
This is the first of a general external validator concept: a callback that performs validation the library can't, and returns a FormanExternalValidationResult verdict ({ valid, errors?, warnings? }) that is spliced into the overall result. The callback may be async (awaited), and its errors/warnings are stamped with the field's domain and path automatically. A valid: false verdict always fails validation, even when it carries no messages.
import { validateForman, type FormanExternalValidationResult } from '@makehq/forman-schema';
import Ajv from 'ajv'; // any JSON Schema validator works
const ajv = new Ajv({ allErrors: true });
const result = await validateForman({ input: { name: 'Alice', age: 30 } }, schema, {
async validateJson(schema, value): Promise<FormanExternalValidationResult> {
const validate = ajv.compile(schema);
if (validate(value)) return { valid: true };
return {
valid: false,
errors: (validate.errors ?? []).map(e => `${e.instancePath} ${e.message}`),
};
},
});Validate Forman values against a Forman Schema. Two entry points are available:
validateForman(values, schema, options?)— validate without domains.validateFormanWithDomains(domains, options?)— validate multiple domains at once.
Both return { valid: boolean, errors: { path: string, message: string }[] }.
import { validateForman } from '@makehq/forman-schema';
const values = { array: [1, 2, 3], text: 'hello' };
const schema = [
{ name: 'array', type: 'array', spec: { type: 'number' } },
{ name: 'text', type: 'text' },
];
const result = await validateForman(values, schema);
// { valid: true, errors: [] }const values = { text: 15, unknown: true };
const schema = [
{
name: 'text',
type: 'text',
},
];
const result = await validateForman(values, schema, { strict: true });
// {
// valid: false,
// errors: [
// { path: 'default.text', message: "Expected type 'string', got type 'number'" },
// { path: 'default', message: "Unknown field 'unknown'" }
// ]
// }const values = { sheet: 'sheet 1', row: 1 };
const schema = [
{
name: 'sheet',
type: 'select',
options: [
{ value: 'sheet 1', nested: [{ name: 'row', type: 'number', required: true }] },
{ value: 'sheet 2' },
],
},
];
const result = await validateForman(values, schema);You can resolve options or nested field stores by providing resolveRemote(path, data).
const values = { sheet: 'sheet 1', column: 'A1' };
const schema = [
{
name: 'sheet',
type: 'select',
options: {
store: 'rpc://sheets',
nested: [{ name: 'column', type: 'select', options: 'rpc://columns' }],
},
},
];
const result = await validateForman(values, schema, {
async resolveRemote(path, data) {
if (path === 'rpc://sheets') return [{ value: 'sheet 1' }, { value: 'sheet 2' }];
if (path === 'rpc://columns') return [{ value: 'A1' }, { value: 'B1' }];
throw new Error('Unknown resource');
},
});Use validateFormanWithDomains to validate cross-domain schemas (e.g., default and additional).
import { validateFormanWithDomains } from '@makehq/forman-schema';
const result = await validateFormanWithDomains(
{
default: {
values: { ... },
schema: defaultSchema
},
additional: {
values: { ... },
schema: additionalSchema
},
},
{
async resolveRemote(path, data) {
// resolve API-backed options/nested fields here
},
},
);- account → number
- aiagent → string
- array → array
- buffer → string
- cert → string
- collection → object
- color → string
- datastore → number
- date → string
- email → string
- file → string
- filename → string
- filestorage → array (of UUID strings)
- filter → array
- folder → string
- hidden → string
- hook → number
- integer → number
- json → object (or its
schemaechoed verbatim when provided — see JSON fields) - keychain → number
- number → number
- path → string
- pkey → string
- port → number
- scenario → string
- select → string with enum
- text → string
- time → string
- timestamp → string
- timezone → string
- uinteger → number
- url → string
- uuid → string
- string → text
- number → number
- boolean → boolean
- object → collection
- array → array
SchemaConversionError is thrown when schema conversion fails. It includes a message and optionally the field that caused the error.
To test the project:
npm testTo build the project:
npm run build # Builds both ESM and CJS versions