Migration
Coming from Form.io
Migrating a form portfolio is usually the reason people put a migration off for
a year. convertFormioForm exists to make it incremental: it converts what it
can, reports what it cannot, and never throws — so you can run it over a
hundred forms and get a work list instead of a stack trace.
Converting a definition
Section titled “Converting a definition”-
Feed it a Form.io form definition.
migrate.ts import { convertFormioForm } from '@streamline-pulse/formkrafter-core'const formioJson = JSON.parse(await readFile('./legacy-form.json', 'utf8'))const { spec, warnings } = convertFormioForm(formioJson) -
Review the warnings. Unknown components are skipped and listed here rather than aborting the conversion.
for (const warning of warnings) console.warn(warning)// e.g. 'button "submit" skipped — FormKrafter renders its own submit action' -
Store the resulting spec, or open it in the builder for a manual pass.
What is mapped
Section titled “What is mapped”| Form.io | FormKrafter |
|---|---|
| Field components (textfield, email, number, day, select, radio, selectboxes, checkbox, textarea, phone, url, signature, address, …) | The matching input bricks, inputMask included |
| Layout: panels, columns, tables, tabs, fieldsets | Layout bricks |
Wizard (display: "wizard") |
stepper with per-step validation |
| datagrid / editgrid | data-grid with per-row validation |
validate.* (required, min/max, lengths, pattern, custom JS) |
Validators; custom JS is auto-wrapped for the sandbox (data/row become dataMap) |
conditional (show/when/eq) and JavaScript logic triggers |
Rules; a conditional takes precedence over an always-hidden flag |
Select dataSrc: url / json |
Remote or static options (valueProperty → valueKey, item templates → labelKey, dotted paths supported). Lists of plain strings or numbers convert to matching label/value pairs |
File components (storage: url, multiple) |
file brick with uploadUrl / multiple configs |
Nested form components |
nested-form bricks with specRef |
| Content / HTML components | content bricks, conditionals preserved |
The converter is regression-tested against real production Form.io forms — wizards, conditional file uploads, cascaded selects and custom validators.
What you gain in weight
Section titled “What you gain in weight”Methodology: gzipped bytes a page actually downloads. FormKrafter v0.16.0
measured in headless Chromium against the published lazy-loading bundle, JS and
CSS together; Form.io @formio/js v5.5.0 measured from its official CDN
bundles (July 2026).
| FormKrafter | Form.io | |
|---|---|---|
| Builder (renderer included) | ~130 KB | ~470 KB (formio.full.min.js) |
| Renderer only | ~130 KB | ~415 KB (formio.form.min.js) |
| Required CSS | 1.6 KB, optional | 129 KB + Bootstrap |
The two FormKrafter rows are identical on purpose: the lazy-loading bundle ships its components in shared chunks, so a renderer-only page downloads the same set as one running the builder. What stays out until you open a rules or JS-options editor is the code editor, roughly 100 KB gzipped on its own.
What you gain at runtime
Section titled “What you gain at runtime”Rendering cost stays flat where it matters. Measured on a real converted production form — a 5-step wizard of 93 bricks (nested layouts, file uploads, cascaded selects) — then on synthetic variants scaling it up to 911 bricks on a single page. Methodology: FormKrafter v0.6.0 production build, headless Chromium via Playwright, median of warm runs; keystroke latency is input-event-to-next-paint, so it includes up to two frames of scheduling.
| Bricks on the page | Mount | validate() |
Keystroke latency (median / p90) |
|---|---|---|---|
| 92 (the real form) | 17 ms | 0.6 ms | 14 ms / 33 ms |
| 456 | 29 ms | 1.0 ms | 14 ms / 29 ms |
| 911 | 45 ms | 1.9 ms | 26 ms / 33 ms |
The number worth noticing is the last column: typing latency does not grow with form size — a keystroke never pays for the bricks it does not touch, and even a 911-brick page stays within the 60 fps frame budget while typing.
Keep your specs small
Section titled “Keep your specs small”Your stored definitions shrink too. Real Form.io exports carry heavy boilerplate — every component serialized with all its defaults — while the converter keeps only what carries meaning and compacts static option lists into a newline string whenever labels equal values. Measured on a portfolio of six production forms:
| Raw JSON | Gzipped | |
|---|---|---|
| Form.io definitions | 277 KB | 40 KB |
| Converted FormKrafter specs | 66 KB (3–5.5× smaller per form) | 18 KB |
For the biggest lists — professions, nationalities — do not embed them at all. The converter warns past 100 static options and recommends:
{ "optionsSource": "remote", "optionsUrl": "/api/professions?q={search}" }The select queries your API, with search-as-you-type support.
{ "optionsSource": "catalog", "optionsRef": "professions" }Resolved through optionSourceService: the list is stored
once and referenced by every form that needs it.
Sandbox instead of eval
Section titled “Sandbox instead of eval”Form.io executes custom validators and logic with real JavaScript. FormKrafter
runs the converted code in a CSP-safe AST interpreter — see
dynamic rules. Converted snippets keep working because
the converter injects compatibility aliases (const data = dataMap; const row = dataMap;),
and they gain the sandbox’s isolation for free.
Next steps
Section titled “Next steps”A project by Streamline Pulse