Skip to content

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.

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][];

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)
}

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
}

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
}

Multi-line text input.

interface TextAreaField extends BaseField<"textarea", string> {
placeholder?: string;
maxLength?: number;
minLength?: number;
}

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
}

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
}

Single selection using radio buttons.

interface RadioField<Opts extends readonly string[]>
extends BaseField<"radio", Opts[number]> {
options: Opts;
}

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
}

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" }]
]
}

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
}

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
}

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;

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]>;
};

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>;
const mainSchema = [
["profile", userProfileSchema], // Reference the definition
["user-profile", userProfileSchema], // Include the definition
// ... other fields
] as const satisfies InternalSchema;

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 object
const 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
};
  1. Use as const: Always use as const when defining schemas to preserve literal types
  2. Unique IDs: Ensure all $id values are unique within a schema
  3. Type Annotations: Use satisfies InternalSchema to catch type errors early
  4. Definition Separation: Keep reusable schema definitions separate from main schemas
  5. Validation: Use field-specific validation options (min/max, pattern, etc.)
  6. Required Fields: Explicitly mark required fields for better UX and type safety
// Helper function for better type inference
export const createSchema = <T extends InternalSchema>(schema: T): T => schema;
const mySchema = createSchema([
// ... field definitions
] as const);
// Extract default values from schema
export 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.