Skip to content

Custom Fields

Use defineFieldKind for the normal extension path. It lets you define parsing, validation, serialization, and small renderer hints without writing describe() or a custom primitive renderer.

import { z } from "zod";
import { createBuiltinRegistry, defineFieldKind } from "mlform/engine";
const scoreField = defineFieldKind({
kind: "score",
schema: z.object({
id: z.string().optional(),
kind: z.literal("score"),
label: z.string(),
min: z.number().default(0),
max: z.number().default(100),
step: z.number().optional(),
ui: z.record(z.string(), z.unknown()).optional(),
}),
value: {
default: () => 0,
normalize: (value) => Number(value ?? 0),
serialize: (value) => value,
},
validate: ({ value, config }) =>
value < config.min || value > config.max ? ["Score is outside the allowed range."] : [],
render: {
widget: "number",
hints: ({ config }) => ({
min: config.min,
max: config.max,
step: config.step ?? 1,
unit: "%",
}),
},
});
const registry = createBuiltinRegistry().registerField(scoreField);
HookPurpose
schemaZod schema for field config.
value.defaultInitial value when no default is provided.
value.normalizeConvert UI or host values into runtime values.
value.serializeConvert runtime values into backend payload values.
validateReturn field-level error messages. May be async.
render.widgetPick a built-in renderer shape like text, number, or select.
render.hintsPass small UI hints to the built-in declarative renderer.

Use defineFieldDefinition only when you need full control over the renderer descriptor or a completely custom primitive component.