StructuredOutputForm
A model that extracts or proposes structured data is usually almost right.
Rather than trusting the JSON, show it as a form: each field becomes the
matching control (Input, NumberInput, Switch, NativeSelect) inside a
Field, so labels, descriptions and ids are wired, and the human fixes
what is wrong before it is used. Fields the model filled carry a
Generated badge until they are edited.
import StructuredOutputForm, {type StructuredField} from '@zuilib/ai/structured-output-form'
Basic
Owner had no value from the model, so it has no badge; the others lose
theirs on the first edit and onFieldChange fires once per field.
Which fields are generated
By default every field that has a value when the form mounts counts as
generated. Pass generated={['seats', 'term']} to say exactly which, for
instance when some values were pre-filled by your own code.
Errors
Validation is yours (the model's output, your schema). Pass
errors={{seats: 'Must be at least 10'}} and the matching field renders
the message under its control as a FieldError (data-slot="structured-output-form-error")
and is marked invalid through Field (a boolean field sets
aria-invalid and aria-describedby on the switch itself). The submit
button stays enabled so the human can fix and resubmit.
Below sm (640px) the buttons stack full width; on a coarse pointer every
control has a 44px target.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fieldsrequired | StructuredField[] | — | {name, label, type, options?, description?, required?, placeholder?} |
valuerequired | Record<string, string | number | boolean | null | undefined> | — | |
onValueChangerequired | (value, changedField: string) => void | — | |
onSubmit | (value) => void | — | |
generated | string[] | — | Defaults to every field with a value on mount |
onFieldChange | (name: string) => void | — | First edit of a generated field |
errors | Partial<Record<string, ReactNode>> | — | Per-field validation messages; a field with one is invalid |
renderField | (props: StructuredFieldControlProps) => ReactNode | undefined | — | Called for every field first; return a control to take it over (label, badge, description and error stay wired), or undefined to keep the built-in |
submitLabel | string | 'Use these values' | |
generatedLabel | string | 'Generated' | |
footer | ReactNode | — | Before the submit button |
showSubmit | boolean | true | |
className | string | — |
type is a built-in — 'string' | 'number' | 'boolean' | 'enum' — or
any name a renderField handles ('date', 'currency'); an unknown
type with no custom control falls back to the text input, so a schema
from a newer producer stays editable. An enum's options are strings or
{value, label} pairs.