32 / Documents
Schema Builder
Build the shape you want extracted from a document. Fields carry a name, type, description, and requirement, and object and array fields nest.
"use client"
import { SchemaBuilder } from "mischief-ui/schema-builder"
export function SchemaBuilderDemo() {
return (
<SchemaBuilder
className="w-full max-w-xl"
defaultFields={[
{
id: "vendor",
name: "vendor",
type: "string",
description: "Who issued the invoice.",
required: true,
},
{
id: "lines",
name: "line_items",
type: "array",
fields: [
{ id: "desc", name: "description", type: "string" },
{ id: "amount", name: "amount", type: "number", required: true },
],
},
]}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/schema-builderimport { SchemaBuilder } from "mischief-ui/schema-builder"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { ChevronRight, Plus, Trash2 } from "lucide-react"import { cn } from "@/lib/utils" export type SchemaFieldType = "string" | "number" | "boolean" | "date" | "object" | "array" export type SchemaField = { id: string name: string type: SchemaFieldType description?: stringUsage
export function Extraction() {
return (
<SchemaBuilder
defaultFields={[{ id: "total", name: "total", type: "number" }]}
onFieldsChange={saveSchema}
/>
)
}The shape it produces
Fields come back as a tree, in the order they were arranged. Object and array fields nest through their own fields array, and everything else is a leaf. This is deliberately not JSON Schema: it is small enough to read, and short enough to convert into whatever your extractor actually wants.
const fields = [
{ id: "1", name: "total", type: "number", required: true },
{
id: "2",
name: "supplier",
type: "object",
fields: [
{ id: "3", name: "name", type: "string", required: true },
{ id: "4", name: "vat", type: "string" },
],
},
]Six types are offered by default -- string, number, boolean, date, object, and array. Narrow that with types when your extractor supports fewer, and cap nesting with maxDepth so nobody builds a structure the other end cannot represent.
Ids for new fields
New fields need an id, and the default generator is fine for a form whose result is read once. Pass createId when the ids are going to outlive the page -- stored, compared, or sent somewhere that expects them to be stable.
<SchemaBuilder
defaultFields={fields}
createId={() => crypto.randomUUID()}
onFieldsChange={save}
/>API
fields, defaultFieldsSchemaField[]The schema, controlled or uncontrolled.onFieldsChange(fields: SchemaField[]) => voidRuns on every edit.typesreadonly SchemaFieldType[]The type options offered. Defaults to string, number, boolean, date, object, and array.maxDepthnumberHow far object and array fields may nest. Defaults to 3.createId() => stringSupplies ids for new fields when you need them stable.SchemaField
idstringUnique across the whole tree.namestringThe field name as it will be extracted.typeSchemaFieldTypestring, number, boolean, date, object, or array.descriptionstringA hint for whoever, or whatever, fills it.requiredbooleanMarks the field as expected.fieldsSchemaField[]Children, on an object or an array.Accessibility
Every input has a label naming the field it belongs to, so a screen reader user knows which row they are editing rather than hearing a run of unlabelled boxes. Nesting controls say which field they open, and remove buttons name the field they delete. Only object and array fields offer nesting, and nesting stops at maxDepth.