Internal Schema System
The internal schema system is the core type-safe schema definition language that powers the JSON UI library. It provides a strongly-typed way to define forms with complex validation, nested objects, dynamic field selection, and more.
Core Concepts
Section titled “Core Concepts”Schema Structure
Section titled “Schema Structure”An InternalSchema is a readonly array of key-value pairs, where each pair consists of:
- Key: A string identifier for the field
- Field: A field definition object extending
BaseField
type InternalSchema = readonly [key: string, field: Field][];Base Field Structure
Section titled “Base Field Structure”All fields extend the BaseField interface:
interface BaseField<T extends FieldType, V> { $id: string; // Unique identifier for the field type: T; // Field type discriminator defaultValue?: V; // Optional default value label?: string; // Display label required?: boolean; // Whether field is required $isDef?: boolean; // Whether this is a definition (not a form field)}Field Types
Section titled “Field Types”Basic Input Fields
Section titled “Basic Input Fields”ToggleField
Section titled “ToggleField”Boolean on/off switch with optional description.
interface ToggleField extends BaseField<"toggle", boolean> { description?: string;}
// Example usage{ $id: "is-enabled", type: "toggle", label: "Enable Feature", description: "Turn this on to enable the feature", required: true}TextField
Section titled “TextField”Single-line text input with validation options.
interface TextField extends BaseField<"textfield", string> { placeholder?: string; maxLength?: number; minLength?: number; pattern?: string; // RegExp pattern for validation}
// Example usage{ $id: "email-field", type: "textfield", label: "Email Address", placeholder: "Enter your email", pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", required: true}TextAreaField
Section titled “TextAreaField”Multi-line text input.
interface TextAreaField extends BaseField<"textarea", string> { placeholder?: string; maxLength?: number; minLength?: number;}NumberField
Section titled “NumberField”Numeric input with range and step controls.
interface NumberField extends BaseField<"numberfield", number> { placeholder?: string; min?: number; max?: number; step?: number;}
// Example usage{ $id: "age-field", type: "numberfield", label: "Age", min: 0, max: 120, step: 1, required: true}Choice Fields
Section titled “Choice Fields”DropdownField
Section titled “DropdownField”Single selection from a list of options.
interface DropdownField<Opts extends readonly string[]> extends BaseField<"dropdown", Opts[number]> { options: Opts;}
// Example usage{ $id: "country-field", type: "dropdown", label: "Country", options: ["USA", "Canada", "UK", "Germany"] as const, required: true}RadioField
Section titled “RadioField”Single selection using radio buttons.
interface RadioField<Opts extends readonly string[]> extends BaseField<"radio", Opts[number]> { options: Opts;}CheckboxesField
Section titled “CheckboxesField”Multiple selection from a list of options.
interface CheckboxesField<Opts extends readonly string[]> extends BaseField<"checkboxes", Opts[number][]> { options: Opts;}
// Example usage{ $id: "skills-field", type: "checkboxes", label: "Skills", options: ["JavaScript", "TypeScript", "React", "Node.js"] as const}Complex Fields
Section titled “Complex Fields”ObjectField
Section titled “ObjectField”Nested object with its own schema.
interface ObjectField<T extends InternalSchema> extends BaseField<"object", InternalSchemaValue<T>> { children: T;}
// Example usage{ $id: "address-field", type: "object", label: "Address", children: [ ["street", { $id: "street", type: "textfield", label: "Street", required: true }], ["city", { $id: "city", type: "textfield", label: "City", required: true }], ["zipCode", { $id: "zip", type: "textfield", label: "ZIP Code" }] ]}ListField
Section titled “ListField”Dynamic array of items of a specific field type.
interface ListField<T extends Field> extends BaseField<"list", FieldValue<T>[]> { itemType: T; minItems?: number; maxItems?: number;}
// Example usage{ $id: "hobbies-field", type: "list", label: "Hobbies", itemType: { $id: "hobby-item", type: "textfield", placeholder: "Enter a hobby" }, minItems: 1, maxItems: 5}FieldSelectorField
Section titled “FieldSelectorField”Dynamic field selection - allows users to choose from predefined field schemas.
interface FieldSelectorField<Fields extends readonly Field[]> extends BaseField< "fieldselector", { value: FieldValue<Fields[number]>; $id: Fields[number]["$id"] } > { fields: Fields;}
// Example usage{ $id: "contact-method", type: "fieldselector", label: "Preferred Contact Method", fields: [emailSchema, phoneSchema, addressSchema], required: true}Type Safety and Value Extraction
Section titled “Type Safety and Value Extraction”FieldValue Type
Section titled “FieldValue Type”The FieldValue<T> type extracts the appropriate TypeScript type for any field:
type FieldValue<T> = T extends ObjectField<infer Schema> ? InternalSchemaValue<Schema> : T extends ListField<infer ItemType> ? FieldValue<ItemType>[] : T extends ToggleField ? boolean : T extends TextField | TextAreaField ? string : T extends NumberField ? number : T extends DropdownField<infer Opts> ? Opts[number] : T extends RadioField<infer Opts> ? Opts[number] : T extends CheckboxesField<infer Opts> ? Opts[number][] : T extends FieldSelectorField<infer Fields> ? { value: FieldValue<Fields[number]>; $id: Fields[number]["$id"] } : never;InternalSchemaValue Type
Section titled “InternalSchemaValue Type”The InternalSchemaValue<T> type creates a TypeScript object type that respects required/optional fields:
type InternalSchemaValue<T extends InternalSchema> = { [P in RequiredKeys<T>]: FieldValue<Extract<T[number], readonly [P, Field]>[1]>;} & { [P in OptionalKeys<T>]?: FieldValue<Extract<T[number], readonly [P, Field]>[1]>;};Schema Definitions vs Form Fields
Section titled “Schema Definitions vs Form Fields”Definition Fields ($isDef: true)
Section titled “Definition Fields ($isDef: true)”Fields marked with $isDef: true are schema definitions that can be referenced but don’t appear as form fields:
const userProfileSchema = { $id: "user-profile", $isDef: true, type: "object", label: "User Profile", children: [ ["name", { $id: "name", type: "textfield", label: "Name", required: true }], ["email", { $id: "email", type: "textfield", label: "Email", required: true }] ]} as const satisfies ObjectField<InternalSchema>;Using Definitions in Schemas
Section titled “Using Definitions in Schemas”const mainSchema = [ ["profile", userProfileSchema], // Reference the definition ["user-profile", userProfileSchema], // Include the definition // ... other fields] as const satisfies InternalSchema;Complete Example
Section titled “Complete Example”Here’s a comprehensive example showing various field types:
const registrationSchema = [ // Basic fields ["name", { $id: "name-field", type: "textfield", label: "Full Name", required: true, minLength: 2, maxLength: 50 }],
["age", { $id: "age-field", type: "numberfield", label: "Age", min: 18, max: 100, required: true }],
// Nested object ["address", { $id: "address-field", type: "object", label: "Address", children: [ ["street", { $id: "street", type: "textfield", label: "Street", required: true }], ["city", { $id: "city", type: "textfield", label: "City", required: true }] ] }],
// List field ["hobbies", { $id: "hobbies-field", type: "list", label: "Hobbies", itemType: { $id: "hobby", type: "textfield" }, maxItems: 3 }],
// Choice fields ["country", { $id: "country-field", type: "dropdown", label: "Country", options: ["USA", "Canada", "UK"] as const, required: true }],
["skills", { $id: "skills-field", type: "checkboxes", label: "Technical Skills", options: ["JavaScript", "TypeScript", "React"] as const }],
// Toggle ["newsletter", { $id: "newsletter-field", type: "toggle", label: "Subscribe to Newsletter", description: "Receive updates about new features" }]] as const satisfies InternalSchema;
// Type-safe value objectconst formValue: InternalSchemaValue<typeof registrationSchema> = { name: "John Doe", age: 25, address: { street: "123 Main St", city: "Anytown" }, hobbies: ["Reading", "Gaming"], country: "USA", skills: ["JavaScript", "TypeScript"], newsletter: true};Best Practices
Section titled “Best Practices”- Use
as const: Always useas constwhen defining schemas to preserve literal types - Unique IDs: Ensure all
$idvalues are unique within a schema - Type Annotations: Use
satisfies InternalSchemato catch type errors early - Definition Separation: Keep reusable schema definitions separate from main schemas
- Validation: Use field-specific validation options (min/max, pattern, etc.)
- Required Fields: Explicitly mark required fields for better UX and type safety
Utilities
Section titled “Utilities”Creating Schemas
Section titled “Creating Schemas”// Helper function for better type inferenceexport const createSchema = <T extends InternalSchema>(schema: T): T => schema;
const mySchema = createSchema([ // ... field definitions] as const);Value Extraction
Section titled “Value Extraction”// Extract default values from schemaexport const getDefaultValues = <T extends InternalSchema>( schema: T): Partial<InternalSchemaValue<T>> => { return Object.fromEntries( schema .filter(([, field]) => !field.$isDef && field.defaultValue !== undefined) .map(([key, field]) => [key, field.defaultValue]) );};This type system ensures complete type safety from schema definition through value handling, making it impossible to have runtime type mismatches when properly implemented.