Fields
How field registration, validation, transforms, and rendering work internally.
Main pieces
The field system is built from:
- field modules under
fields/coreandfields/custom, - the registry in fields/registry.ts,
- schema assembly in lib/schema.ts,
- field rendering in the entry form.
Field modules
A field module can export:
labelschemadefaultValuereadwriteEditComponentViewComponent
The registry collects these exports and exposes them to the rest of the app.
Registry
fields/registry.ts registers core fields directly and then registers custom fields from the generated manifest in custom.generated.ts.
That generated file is written by next.config.mjs by scanning fields/custom/*/index.ts(x).
The custom field folder name becomes the field type. For example:
fields/custom/my-field/index.tsx
registers the field as type: my-field.
That produces shared maps for:
- labels,
- schemas,
- default values,
- read functions,
- write functions,
- edit components,
- view components.
These maps are then used throughout the app.
Validation pipeline
Form validation is assembled in lib/schema.ts.
High-level flow:
generateZodSchema(fields)walks the configured field tree.- For each field type, it looks up the registered
schema. - Object and block fields are wrapped recursively.
- List behavior is applied on top when
field.listis enabled. - Required/optional handling is applied around the result.
This means your field-level schema usually only needs to describe the field itself, not the whole surrounding object structure.
Default values
Initial editor state is built with initializeState(...) in lib/schema.ts.
Resolution order is:
- explicit field
default, - list default if present,
- registered
defaultValue, - fallback empty value.
Read and write transforms
read and write are for storage/UI conversion.
Typical pattern:
readruns when content is loaded into the editor,writeruns when editor values are serialized back to content.
Use them when stored values should not match the editor representation one-to-one.
Rendering pipeline
The entry form resolves the field type to its registered EditComponent.
That component receives props such as:
valueonChangefield
The component is responsible for:
- rendering the editing UI,
- converting browser events into field values,
- calling
onChangewith the value expected by the field schema and write pipeline.
ViewComponent is the compact display version used outside the main form editing flow.
Lists vs field-specific multiple behavior
There are two different concepts:
field.list
This means the field itself is repeated as a list item by the form system.- field-specific
options.multiple
This means a single field manages multiple selections internally, such as select/reference.
These are separate layers and should not be conflated.
Good design rules for field authors
- keep
schemanarrow and explicit, - keep
EditComponentdumb where possible, - use
read/writeonly when representation actually differs, - avoid leaking transport or API concerns into generic field logic,
- copy a simple field first, then add complexity.