Author With Helpers
Use defineForm, field, layout helpers, and rule helpers to create type-aware Form Definitions.
The fastest way to write Schemaform definitions in application code is the helper API:
import { defineForm, field } from "@mackehansson/schemaform";The helpers do not create a second model. They produce a normal FormDefinition with formatVersion, schema, uiSchema, and rules. That means you can pass the result directly to <SchemaForm />, store it in your database, inspect it in your builder, or continue editing it with the lower-level builder mutations.
1. Define Fields First
Start with fields. These keys become the type-safe vocabulary for pages, layouts, and rules.
const signupForm = defineForm({
fields: {
email: field.email({ title: "Email", required: true }),
plan: field.select({
title: "Plan",
required: true,
options: ["Free", "Enterprise"],
}),
companyName: field.text({ title: "Company name" }),
},
pages: ({ page }) => [page("Signup", ["email", "plan", "companyName"])],
});When you type the array passed to page, TypeScript can suggest email, plan, and companyName. A typo is caught before the form reaches the renderer.
2. Add App Metadata
Use meta for presentation data that belongs to your renderer or widgets. Schemaform stores and resolves it, but does not interpret it.
const signupForm = defineForm({
fields: {
email: field.email({
title: "Email",
required: true,
meta: {
iconText: "Please provide an email",
},
}),
},
});A custom widget can read the value from the resolved control:
function EmailWidget({ control }: { control: ResolvedControl }) {
return <p>{control.meta?.iconText}</p>;
}The helper options stay strict, so misspelling required is still a type error. App-specific data goes under meta.
To make meta fully typed in your app, augment the exported interface:
import "@mackehansson/schemaform";
declare module "@mackehansson/schemaform" {
interface SchemaformMeta {
iconText?: string;
}
}3. Add Layout
Use layout helpers when the form needs structure. A field key creates a Control node. A helper creates a layout node.
const applicationForm = defineForm({
fields: {
firstName: field.text({ title: "First name", required: true }),
lastName: field.text({ title: "Last name", required: true }),
email: field.email({ title: "Email", required: true }),
companyName: field.text({ title: "Company name" }),
},
pages: ({ page, group, columns }) => [
page("Application", [
group("Applicant", [columns(["firstName", "lastName"]), "email"]),
group("Company", ["companyName"], { id: "company-section" }),
]),
],
});The renderer still receives UI Schema. It does not know or care that helpers created it.
4. Add Declarative Rules
Rule helpers write the persisted rule objects for common conditions:
const signupForm = defineForm({
fields: {
plan: field.select({
title: "Plan",
options: ["Free", "Enterprise"],
}),
companyName: field.text({ title: "Company name" }),
},
pages: ({ page, group }) => [
page("Signup", [
"plan",
group("Company", ["companyName"], { id: "company-section" }),
]),
],
rules: ({ rule, target }) => [
rule.when("plan").equals("Enterprise").show(target("company-section")),
rule.when("plan").equals("Enterprise").require("companyName"),
],
});Use a field key when the rule targets a field. Use target("layout-id") when it targets a layout node.
5. Use Helpers In Builders
For a custom form builder, helpers are useful in places where your app creates definitions programmatically:
- starter templates
- test fixtures
- import mappers
- template-preset buttons
- examples shown in product documentation
For direct canvas edits, use useFormBuilder and the pure mutation operations. Those APIs are better for incremental updates because they preserve existing pages, selection state, and undo history.
6. Drop Down When Needed
Helpers are the friendly authoring surface. The lower-level APIs are still available when you need exact control:
import {
addField,
addRule,
migrate,
parseDefinition,
resolveForm,
} from "@mackehansson/schemaform/core";Both levels operate on the same FormDefinition, so you can author with helpers, render with <SchemaForm />, and later edit with builder mutations without converting models.