| title | Fields API |
|---|---|
| path | field-reference |
| summary | All 22 built-in field kinds, common schema properties, type-specific options, inferred value shapes, defaults, validation, hooks, localization, and admin overrides. |
Companions:
- Fields — recipes and explanation for schema helpers, admin helpers, component slots, conditions, and cross-field writes.
- Collections API — the collection and block definitions that contain field arrays.
- Relationships — relation envelopes, population, filters, and
hasManybehavior. - File and media uploads —
UploadConfig, stored file values, storage routing, variants, and upload hooks. - Rich text — Lexical
EditorConfig, extensions, relation modes, and server adapters.
This reference lists every field kind accepted by CollectionDefinition.fields and Block.fields. A field is a discriminated object whose type selects its value shape and type-specific options.
Every field accepts these properties in addition to its required name and type.
| Property | Default | Description |
|---|---|---|
name |
Required | Stable field key. It participates in schema paths, generated types, storage paths, and API values. Reserved system names are rejected. |
type |
Required | One of the 22 field discriminators listed below. |
label |
Derived by the UI | Human-readable admin label. |
helpText |
None | Supporting text rendered with the input. |
placeholder |
None | Placeholder hint forwarded by widgets that support it. |
optional |
false |
Makes the generated property optional and permits an absent value. Fields are required by default. |
readOnly |
false |
Admin rendering hint. It is not server-side immutability or an authorization boundary. |
hooks |
None | Client-side beforeValidate and beforeChange hook slots. |
condition |
None | Client-side visibility predicate over the full form data and current sibling scope. Hidden values remain stored. |
virtual |
false |
Makes the value available to form state and lifecycle hooks but excludes it from persistence. |
validate |
None | Content-save validator (value, data) => string | undefined available on every field, including structure fields. |
localized |
false |
Available only on localizable field kinds. Stores a distinct value per content locale when true. |
An optional field becomes an optional property in inferred and generated object types. The value type also permits undefined.
{ name: 'subtitle', type: 'text', optional: true }
// Inferred shape: subtitle?: string | undefinedreadOnly prevents normal editing in Byline’s built-in admin widgets. Groups, arrays, and blocks pass the hint to their descendants and disable structural changes. File and image widgets retain preview and download actions. Custom field components and rich-text editor replacements must implement the hint themselves. API clients can still submit the field. Enforce security and immutability in abilities, hooks, lifecycle services, or a field-specific server rule.
type FieldCondition = (
data: Record<string, any>,
siblingData: Record<string, any>
) => booleanThe admin re-evaluates the condition as form data changes. Hidden fields retain their values and are exempt from client validation while hidden. condition is a rendering hint: the lifecycle does not evaluate it, so a conditionally hidden field that is not optional and has no default is still required on the server, and the editor cannot satisfy it because the field is not rendered.
Express a requirement that applies only in particular circumstances explicitly, with optional: true plus a validate callback that decides from the document. The callback runs on both sides — the lifecycle enforces it, and the admin reports it wherever the field is visible:
{
type: 'text',
name: 'doi',
optional: true,
condition: (data) => data.kind === 'journal',
validate: (value, data) =>
data.kind === 'journal' && !value ? 'DOI is required for journal articles' : undefined,
}Do not use condition to express requiredness. Beyond the contract above, restores and duplications validate with locale: 'all', where a localized value is a per-locale map rather than the single value the editor supplies, so a predicate reading one can disagree with itself across those paths. A validate callback that must run under 'all' should account for the same shape.
{
name: 'thumbnailCaption',
type: 'text',
optional: true,
condition: (_data, siblings) => Boolean(siblings.generateThumbnail),
}A virtual value reaches beforeCreate, afterCreate, beforeUpdate, and afterUpdate, but the storage flatten pass writes no row for it. It is absent on the next read.
Boot validation enforces these constraints:
- it must be optional or declare a
defaultValue; - it cannot be a
counter; - an upload-capable
fileorimagecannot be virtual; and useAsTitle,useAsPath, and search configuration cannot reference it.
Setting virtual on group, array, or blocks omits the entire subtree.
Byline has three structure fields and 19 value fields.
type |
Value shape | Localizable | Type-specific properties |
|---|---|---|---|
group |
Object from nested fields | No | fields |
array |
Ordered array of { _id, ...fields } |
No | fields, validation.minLength, validation.maxLength |
blocks |
Ordered discriminated block array | No | blocks |
text |
string |
Yes | defaultValue, length/pattern/rule validation |
textArea |
string |
Yes | defaultValue, length/pattern/rule validation |
code |
string |
Yes | defaultValue, language, languageField, length/rule validation |
checkbox |
boolean |
No | defaultValue |
boolean |
boolean |
No | defaultValue |
select |
Declared option-value union | No | options, defaultValue |
richText |
JSON value | Yes | defaultValue, length validation, editorConfig, relation-mode flags |
time |
string |
No | defaultValue |
date |
Date |
No | defaultValue |
datetime |
Date |
No | mode, yearsInFuture, yearsInPast, defaultValue |
float |
number |
No | defaultValue, validation.min, validation.max |
integer |
number |
No | defaultValue, validation.min, validation.max |
decimal |
string |
No | defaultValue |
counter |
number |
No | group |
json |
JSON value | Yes | defaultValue |
object |
JSON object | Yes | defaultValue |
relation |
Relation envelope or ordered envelope array | No | targetCollection, displayField, hasMany, minItems, maxItems |
file |
StoredFileValue |
No | upload |
image |
StoredFileValue |
No | upload |
Array and block _id values are synthetic identity metadata. They stabilize editing and ordering but are not user schema data.
interface GroupField {
name: string
type: 'group'
fields: readonly Field[]
}Groups assemble nested fields into one object without creating a repeated item boundary.
{
name: 'seo',
type: 'group',
fields: [
{ name: 'title', type: 'text', optional: true },
{ name: 'description', type: 'textArea', optional: true },
],
}interface ArrayField {
name: string
type: 'array'
fields: readonly Field[]
validation?: { minLength?: number; maxLength?: number }
}Each stored item gains a synthetic _id. Array values preserve order.
{
name: 'authors',
type: 'array',
validation: { minLength: 1, maxLength: 10 },
fields: [
{ name: 'name', type: 'text' },
{ name: 'role', type: 'text', optional: true },
],
}interface BlocksField {
name: string
type: 'blocks'
blocks: Block[]
}Each block value contains synthetic _id identity and a _type discriminator equal to the definition's blockType.
{
name: 'content',
type: 'blocks',
blocks: [RichTextBlock, PhotoBlock, QuoteBlock],
}interface TextFieldOptions {
defaultValue?: DefaultValue<string>
validation?: {
minLength?: number
maxLength?: number
pattern?: string
rules?: ValidationRule[]
}
}Both store strings and support localization. They differ in their default admin widget.
interface CodeFieldOptions {
defaultValue?: DefaultValue<string>
language?: string
languageField?: string
validation?: {
minLength?: number
maxLength?: number
rules?: ValidationRule[]
}
}language sets the default syntax-highlighting language. languageField names a sibling string field whose live value overrides it. Both affect presentation only; storage contains the source string.
Both store boolean and accept defaultValue?: DefaultValue<boolean>. They remain separate discriminators so admin widgets and semantics can differ.
interface SelectFieldOption {
label: string
value: string
}
interface SelectField {
type: 'select'
options: [SelectFieldOption, ...SelectFieldOption[]]
defaultValue?: DefaultValue<string>
}The options array must be non-empty. defineCollection() preserves literal values so generated types become their union.
{
name: 'format',
type: 'select',
options: [
{ label: 'Article', value: 'article' },
{ label: 'Report', value: 'report' },
],
defaultValue: 'article',
}interface RichTextFieldOptions {
defaultValue?: DefaultValue<unknown>
validation?: { minLength?: number; maxLength?: number }
editorConfig?: unknown
embedRelationsOnSave?: boolean
populateRelationsOnRead?: boolean
}| Property | Default | Description |
|---|---|---|
editorConfig |
Adapter defaults | Serializable editor-specific data. For Lexical this is EditorConfig. It does not replace the React editor component. |
embedRelationsOnSave |
true |
Refreshes relation-bearing nodes during writes through the registered server embed adapter. |
populateRelationsOnRead |
Inverse of embedRelationsOnSave |
Refreshes relation-bearing nodes during reads through the registered server populate adapter. |
At least one relation mode must be effective. Setting both flags to false fails boot validation. Setting both to true embeds on write and refreshes on read.
Rich text documents Lexical configuration, extensions, markdown, toolbar contributions, and server adapters.
Stores a time string and accepts defaultValue?: DefaultValue<string>.
Uses a Date value and accepts defaultValue?: DefaultValue<Date>.
interface DateTimeFieldOptions {
mode?: 'date' | 'datetime'
yearsInFuture?: number
yearsInPast?: number
defaultValue?: DefaultValue<Date>
}mode controls the admin input mode. The year-range values bound the date picker.
interface NumericFieldOptions {
defaultValue?: DefaultValue<number>
validation?: { min?: number; max?: number }
}Both restore as JavaScript number; integer uses integer storage and validation semantics.
Decimal values are precision-preserving strings, not JavaScript numbers.
{ name: 'price', type: 'decimal', defaultValue: '0.00' }interface CounterField {
type: 'counter'
group: string
}The lifecycle allocates an immutable monotonically increasing integer at create time. Counter fields sharing the same stable group share one sequence across collections. Gaps can occur after rollbacks and deletes. A counter has no default or validation slot and cannot appear inside arrays or blocks.
Stores any JSON value and supports localization.
type JsonValue = string | number | boolean | null | JsonValue[] | JsonObjectStores a JSON object and supports localization.
type JsonObject = { [key: string]: JsonValue }interface RelationField {
type: 'relation'
targetCollection: string
displayField?: string
hasMany?: boolean
minItems?: number
maxItems?: number
}| Property | Default | Description |
|---|---|---|
targetCollection |
Required | Target collection path. |
displayField |
Target identity fallback | Target field displayed in the picker and inline summary. |
hasMany |
false |
Changes the value from one envelope to an ordered array of envelopes. |
minItems |
None | Minimum array length when hasMany is true. |
maxItems |
None | Maximum array length when hasMany is true. |
The canonical unpopulated value is RelatedDocumentValue; population adds a target ClientDocument under .document. Relationships documents all envelope states and the populate DSL.
interface FileField {
type: 'file'
upload?: UploadConfig
}
interface ImageField {
type: 'image'
upload?: UploadConfig
}Both restore a StoredFileValue. Adding upload makes that field upload-capable and mounts it on the collection upload transport. Image processing depends on MIME type and configured sizes, not on whether the discriminator is file or image.
The complete UploadConfig, ImageSize, storage precedence, hook, transport, and stored-value contracts are in File and media uploads.
Value fields that expose defaultValue accept a literal or a synchronous/async factory:
type DefaultValue<T> =
| T
| ((ctx: {
data: Record<string, any>
locale?: string
now: () => Date
uuid?: () => string
}) => T | Promise<T>){
name: 'publishedOn',
type: 'datetime',
defaultValue: ({ now }) => now(),
}Core lifecycle services validate the prepared document immediately before writing a content version. This includes create, update, patch saves, duplication, locale copies, and version restores. Validation runs after lifecycle hooks and normalization, recursively checking groups, array items, and declared block variants. Required fields and declared scalar constraints apply to drafts as well as published content. An older document missing a newly required field remains readable, but its next content save must supply that field. Metadata-only path, advertised-locale, and status operations do not validate or rewrite content.
The admin performs the same recursive checks before submission, while exempting condition-hidden fields and pending uploads. The server does not exempt hidden fields. Supply required values through the editor, the caller, or lifecycle hooks; a schema defaultValue alone does not fill a missing SDK write value. An omitted optional container is accepted; a present container must satisfy its child fields. Counter allocation remains lifecycle-owned.
Field errors use ERR_VALIDATION with details.reason: 'invalid_document_fields' and an issues array of { field, message, kind }, where kind is required for a declared value that is absent and invalid for every other failure, including a validate callback's message. getDocumentFieldValidationDetails(error) from @byline/core safely decodes this contract from live or serialized errors. Array and block paths use stable item identities when available, for example content[id=abc].title. The admin displays these errors without discarding edits. Rich-text content remains editor-defined JSON; core does not impose a Lexical document schema or measure its rendered text length.
Restoring a version is exempt from this gate. A historical version cannot be corrected through ordinary editing before it is restored, so enforcing today's rules on it would make the content permanently unrecoverable. Two independent things can put a historical version out of step:
- The schema moved. A field became required, a bound tightened, a block type was removed.
- The rules moved, while the schema stood still. Validation itself changed, so a version whose collection never changed can still fail. Corrected
emailandurlrules are a concrete case.
A restore therefore persists whatever the version holds. It remains subject to authorization, revision and version-ownership checks, hooks, and storage constraints; only the field-validation gate is skipped. The issues are still computed and returned as validationIssues on the restore result, and the admin reports them so you know what to fix before the next ordinary save — which is validated in full.
Two consequences are deliberate recovery semantics rather than guarantees about restored content:
- A
beforeUpdatehook on a restore runs inside the exemption, so content it produces is not validated either. - Restore uses the collection's configured default status. A workflow whose
defaultStatusispublished—SINGLE_STATUS_WORKFLOW, or any workflow configured that way — therefore republishes restored content that fails current validation.
Read the exemption narrowly. Create, update, patch saves, duplication and locale copies all keep the gate: each reads or authors content the editor can open and correct, so a refusal there is a prompt to act. Publication is a separate boundary again — workflow status is lifecycle metadata rather than content, so changeStatus mutates it in place without validating fields, and an incomplete draft can reach published through an ordinary transition whether or not a restore put it there. Content validation gates content writes; it is not a publication gate. Enforce publication preconditions in abilities or in the beforeStatusChange hook, which is the slot that transitions actually invoke — beforeUpdate does not run on a status change, so a precondition placed there would not guard that pathway. Scheduled publication invokes beforeStatusChange as well.
One limitation is not addressed here: a historical version may be incompatible with the current schema in ways validation cannot see, because reconstruction resolves stored rows against today's field set and does not preserve data it no longer recognises. Schema-aware restoration — identifying the schema a version was written under, reconstructing without loss, and repairing a restore candidate before committing it — is separate future work.
Type-specific validation objects provide declarative length, range, pattern, or rule checks. Supported generic rules are:
interface ValidationRule {
type: 'min' | 'max' | 'pattern' | 'custom' | 'email' | 'url'
value: any
message?: string
}Every field also accepts a synchronous validator used by the admin precheck and the authoritative lifecycle check:
validate?: (value: any, data: Record<string, any>) => string | undefinedReturn a non-empty string to block submission and display the field error.
interface FieldHooks {
beforeValidate?: FieldBeforeValidateFn | FieldBeforeValidateFn[]
beforeChange?: FieldBeforeChangeFn | FieldBeforeChangeFn[]
}Both receive the changing value, previous value, full form data, instance path, field definition, operation (change or submit), and setFieldValue(path, value).
| Hook | Effect |
|---|---|
beforeValidate |
Runs before built-in validation. Returning { error } is advisory: the value is still committed, but the error is displayed. |
beforeChange |
Runs before the form write. Returning { value } replaces the value; returning { error } blocks the write. |
setFieldValue() writes another field and emits its patch without running the target field's hooks, preventing hook recursion.
interface FieldAdminConfig {
components?: {
Label?: SlotComponent<FieldLabelSlotProps>
HelpText?: SlotComponent<FieldHelpTextSlotProps>
Field?: SlotComponent<FieldInputSlotProps>
beforeField?: SlotComponent<FieldAdornmentSlotProps>
afterField?: SlotComponent<FieldAdornmentSlotProps>
}
editor?: RichTextEditorComponent
}| Property | Description |
|---|---|
components.Label |
Replaces the default label. |
components.HelpText |
Replaces the default help text. |
components.Field |
Replaces the complete value-field input and must call onChange. |
components.beforeField |
Renders between label and input. |
components.afterField |
Renders between input and help text. |
editor |
Replaces the React editor for this rich-text field only. Ignored for other field types. |
Collection overrides are keyed by index-free schema path in CollectionAdminConfig.fields. Block overrides are keyed relative to the block root in BlockAdminConfig.fields.
| Symbol | Purpose |
|---|---|
defineField(definition) |
Identity helper that preserves one field's literal type information. |
CollectionFieldData<C> |
Infers a collection's ordinary single-locale field object. |
CollectionFieldDataAllLocales<C> |
Infers its all-locales field object. |
BlockFieldData<B> |
Infers one block's ordinary field object. |
BlockFieldDataAllLocales<B> |
Infers one block's all-locales field object. |
isStructureField(field) |
Narrows to group, array, or blocks. |
isArrayField(field) |
Narrows to ArrayField. |
isBlocksField(field) |
Narrows to BlocksField. |
isGroupField(field) |
Narrows to GroupField. |
isValueField(field) |
Narrows to the 19 value-field kinds. |