Component

Form Schema

Definitions + data

How typed form definitions relate to JSON Schema, form data, storage metadata, and future form-builder workflows.

Model

Definitions are not submission data

A form system stores at least two different things. The definition describes how to render, validate, and eventually persist a form. The data is the object a user creates by filling that form in.

The split is similar to the model described in Form.io's schema/submission model: the definition and the submitted values are related, but they should not be stored as one thing.

DOM Studio uses a typed form definition as the practical authoring format. It can normalize to renderable children and project toward JSON Schema when a portable data contract is needed.

Example

Definition and values

Form definition

The definition records the data type first. A component can be inferred from that type, or overridden when the default renderer is not specific enough.

const inviteFormDefinition = {
	type: 'DomForm',
	properties: {
		name: {
			type: 'string',
			label: 'Name',
			required: true,
			maxLength: 150,
			storage: {
				column: 'name',
				type: 'varchar',
				length: 150,
				nullable: false,
			},
		},
		email: {
			type: 'email',
			label: 'Email',
			required: true,
			storage: {
				column: 'email',
				type: 'varchar',
				length: 254,
				nullable: false,
				unique: true,
			},
		},
	},
};

Form data

The model only contains the values keyed by field names.

const inviteFormData = {
	name: 'Maya Patel',
	email: 'maya@example.com',
};

Normalization

Author tersely, render explicitly

Keyed properties match JSON Schema's object shape and avoid repeating the field name inside every child. During normalization, those keys become props.name.

The canonical render shape keeps both type and component. The type describes the data. The component describes the editor currently used to manage that data.

const normalized = {
	type: 'object',
	component: 'DomForm',
	children: [
		{
			type: 'string',
			component: 'DomTextInput',
			props: {
				name: 'name',
				label: 'Name',
				required: true,
				maxLength: 150,
			},
		},
		{
			type: 'email',
			component: 'DomEmailInput',
			props: {
				name: 'email',
				label: 'Email',
				required: true,
			},
		},
	],
};

JSON Schema

Use pure schema for the data contract

Pure JSON Schema is useful when the object shape needs to be understood by tools outside DOM Studio. It describes object properties, primitive types, required fields, formats, enum values, and common validation constraints.

Pure JSON Schema does not know which DOM Studio component should manage each value. The adapter can choose defaults from the type and format, then accept decoration when a richer component is needed.

const inviteJsonSchema = {
	$schema: 'https://json-schema.org/draft/2020-12/schema',
	type: 'object',
	required: ['name', 'email'],
	properties: {
		name: {
			type: 'string',
			title: 'Name',
			maxLength: 150,
		},
		email: {
			type: 'string',
			format: 'email',
			title: 'Email',
		},
	},
};

Decoration

Decorate pure schema with DOM Studio metadata

When a pure JSON Schema needs form-specific information, use a vendor extension such as x-el. JSON Schema validators can ignore unknown extension keys, while DOM Studio adapters can read them to choose components, placeholders, option labels, inspector hints, and other UI-only details.

Decoration can also come from adapter options when the data schema must stay completely clean. Both approaches should normalize to the same DOM Studio render definition.

const decoratedJsonSchema = {
	type: 'object',
	required: ['name', 'role'],
	properties: {
		name: {
			type: 'string',
			title: 'Name',
			'x-el': {
				props: {
					placeholder: 'Maya Patel',
				},
			},
		},
		role: {
			type: 'string',
			enum: ['viewer', 'editor', 'admin'],
			title: 'Role',
			'x-el': {
				component: 'DomSelectInput',
				props: {
					options: [
						{ label: 'Viewer', value: 'viewer' },
						{ label: 'Editor', value: 'editor' },
						{ label: 'Admin', value: 'admin' },
					],
				},
			},
		},
	},
};

Relationship

Four layers

LayerPurposeExample
Form definitionThe authored DOM Studio shape: semantic type, optional component override, labels, validation, layout, permissions, and future storage metadata.email: { type: "email", required: true }
Form dataThe values collected from a rendered form. It should stay small and portable.{ email: "maya@example.com" }
JSON SchemaThe portable data contract: object properties, primitive types, required fields, formats, enum values, and common constraints.properties.email format: email
Database schemaThe persistence shape: table, column, datatype, length, nullability, indexes, and relationships.users.email varchar(254) not null unique
Transform lifecycleThe conversion layer between UI values and database values.fromDatabase(value), toDatabase(value)

Direction

Generate either way

A database table is often enough to generate a useful first form: strings become text inputs, booleans become toggles, dates become date pickers, required columns become required fields. Product teams can then add richer UI context: labels, help text, grouping, conditional visibility, validation messages, and component choices that the raw database cannot know.

The reverse should also work. A form builder can let someone pick front-end components, and each component can contribute a sensible storage default. That form definition can generate table metadata or migrations later.

// Database first.
const userTable = {
	name: 'users',
	columns: [
		{ name: 'name', type: 'varchar', length: 150, nullable: false },
		{ name: 'email', type: 'varchar', length: 254, nullable: false },
	],
};

const formDefinition = createFormDefinitionFromTable(userTable);

// Form builder first.
const tableSchema = createTableSchemaFromForm(formDefinition);

Lifecycle

Transform at the boundary

Components should eventually know how their values travel through the system. The browser value, the submitted form value, and the database value are usually close, but they are not always identical. Keeping conversion functions near the field definition makes that lifecycle explicit.

const textFieldDefinition = {
	type: 'string',
	component: 'DomTextInput',
	storage: {
		type: 'varchar',
		length: 150,
	},
	fromDatabase(value) {
		return value ?? '';
	},
	toDatabase(value) {
		return String(value || '').trim();
	},
};

Current scope

Frontend first

Right now, DOM Studio concentrates on the frontend contract: DomForm normalizes typed definitions into renderable children and stores user input in v-model.

The next layer can add storage metadata without changing that core relationship. The form definition remains the master UI definition; JSON Schema can represent the portable data contract; the data remains the values; server adaptors can translate between the definition and real database tables when that part of the stack is ready.