Fields
Choose a field by the value you need to store and the authoring control you want.
Fields are the building blocks of a Ridu document. One Go definition drives operation-engine validation, storage, migrations, generated Go and TypeScript types, REST/OpenAPI contracts, and the control rendered in the admin.
Field anatomy
The constructor chooses the value kind, its first argument becomes the document property, and typed options refine validation and presentation:
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
)
var Posts = ridu.Collection{
Slug: "posts",
Fields: []field.Definition{
field.Text("title", field.Required()),
field.Select(
"status",
field.Choices(
field.Choice{Value: "draft", Label: "Draft"},
field.Choice{Value: "published", Label: "Published"},
),
field.Default("draft"),
),
},
}title and status are public paths in JSON, generated clients, queries, access maps, hooks, and
migrations. Labels and descriptions are author-facing metadata and never rename stored data.

Names are data contracts
A field name starts with a lowercase letter, contains letters/numbers/underscores, and is unique
among siblings. id, createdAt, updatedAt, and deletedAt are framework-owned. Nested names
form paths such as seo.description; runtime issue paths include row indexes such as
links.2.label.
Treat a field rename as an API and data migration. Change field.Label when only author copy should
change.
Choose a built-in field
Start with the value shape consumers should receive, then choose its authoring control.
| Group | Field pages | Value |
|---|---|---|
| Scalar and choice | Text, Textarea, Email, Code, Date, Number, Checkbox, Select, Radio, Slug | Strings, numbers, booleans, and finite choices |
| Structured | JSON, Point, Group, Array, Blocks, Tabs | JSON values, objects, repeated rows, and discriminated lists |
| Relationship and media | Relationship, Upload | Stable document references or populated documents |
| Layout | Row, Collapsible, UI | No stored property; these organise the editor |
| Computed and plugin | Join, Virtual, Plugin | Response-only output or a compiled plugin contract |
Scalar fields
Use Text for a short string, Textarea for multi-line plain text, Code for a syntax-aware editor, and Email for server-validated addresses. Date keeps an explicit string format. Number and Checkbox use JSON number/boolean values. Select and Radio generate unions from configured values; Select can also be multiple. Slug adds deterministic source generation, required/unique/indexed behavior, and manual overrides to ordinary text storage.
Slugs
field.Text("title", field.Required()),
field.Slug("slug", "title"),Slugs are required, unique, indexed, non-localized strings. Generated values follow the source until an author supplies a manual value. Read Slug for ASCII normalization and migration implications.
Field options
Constructors accept narrow option interfaces, making many invalid combinations a Go compile error.
Compatibility that depends on the concrete kind—for example Language on anything except Code—is
reported by ridu.Resolve with an exact config path.
| Option | Purpose |
|---|---|
Label, Description, Placeholder and translation variants |
Author copy without changing data paths |
Required |
Reject missing, null, and kind-specific empty values |
Default, DefaultChoices |
Write supported create defaults |
Unique, Index |
Declare scalar/reference storage invariants or query indexes |
Localized |
Store an independent content value per configured locale |
ReadOnly, Hidden, Sidebar, Columns, Tab |
Change admin presentation, never authorization |
ShowWhen, ShowWhenCondition |
Reactively display controls from typed sibling/document predicates |
AdminComponent |
Select an exact renderer from a statically paired plugin |
| Kind-specific options | Length/range, choices, relationship targets, nested fields, rows, blocks, joins, and editor hints |
ShowWhen is presentation only. Hidden values remain in the form and still pass through
validation, access, hooks, and writes. Use application validation/hooks for a cross-field invariant
and field access for security.
Custom components
Use field.AdminComponent when the stored value and server behavior of a built-in field are right,
but its authoring control is not. The selected Svelte component replaces the field editor without
changing storage, validation, access, hooks, migrations, REST, OpenAPI, or generated types:
field.Text(
"headline",
field.Required(),
field.AdminComponent(
"editorial-tools",
"Headline",
json.RawMessage(`{"showCharacterCount":true}`),
),
)Register the exact renderer in the statically paired admin package:
import { ADMIN_PLUGIN_API_VERSION, defineAdminPlugin, defineFieldPlugin } from '@riducms/plugin';
import HeadlineField from './headline-field.svelte';
const headlineField = defineFieldPlugin({
type: 'text',
key: 'editorial-tools',
componentKey: 'Headline',
component: HeadlineField,
canRender: (field) =>
field.admin.component?.plugin === 'editorial-tools' &&
field.admin.component.component === 'Headline'
});
export const editorialToolsAdminPlugin = defineAdminPlugin({
apiVersion: ADMIN_PLUGIN_API_VERSION,
key: 'editorial-tools',
pairingVersion: 1,
fields: [headlineField]
});The component receives FieldComponentProps: the resolved schema field, the narrow form controller,
admin translations, and an optional authoring host for document lookup, the reference browser, and
plugin requests. Read and write through form.get(field.path) and form.set(field.path, value),
register the path, respect field.admin.readOnly, and render form.issuesFor(field.path). Component
config is public deterministic manifest metadata, so never place secrets or executable policy in it.
The paired backend plugin and admin package must use the same plugin key, admin API version, and
pairing version; generation or startup rejects a missing or incompatible pair.
Use a UI field for presentation that stores no value. Use a Plugin field and the complete custom-field guide when the value shape, validation, generated types, or query behavior is itself new.
Ridu’s component surface is deliberately smaller than Payload’s current field-component API:
| Authoring need | Ridu contract |
|---|---|
| Replace the edit control | field.AdminComponent plus an exact FieldPlugin renderer |
| Add presentation-only editor UI | field.UI with an optional paired renderer |
| Customize array or block row headings | field.RowLabelComponent plus a registered row-label component |
| Customize a collection list cell | Admin plugin listCells, scoped to an exact collection and field |
| Field-specific filter or version diff | No public custom renderer yet |
| Separate label/description/error slots | No independent slots; the replacement field component renders these through its schema metadata and form issues |
| Before/after-input component slots | No independent slots; compose the additional UI inside the replacement field component |
| Server-rendered admin components | Not supported; admin extensions are statically bundled Svelte components and production has no JavaScript server |
Custom components are presentation, not authorization. Ridu evaluates visibility and field access before rendering, but authoritative validation and access remain in Go and apply to every transport.
Defaults and validation
Built-in kinds validate through the operation engine for Local API, REST, SDK, admin, tasks, and
plugin transports. Defaults run on create, not update. Core fields do not accept an
arbitrary Validate callback; normalize input in BeforeValidate, enforce cross-field business
rules in hooks, or create a typed Plugin field with a server validator.
Read sibling values
A sibling is another field under the same parent. At the collection root, every other root field is a sibling. Inside a Group, Array row, or Block, only fields in that object or row are siblings.
Use the API that matches the behavior you are implementing:
| Where you need the value | Read it from | Scope |
|---|---|---|
| Virtual resolver | ctx.Document.Values["name"] |
The current root document |
| Field-access rule | ctx.SiblingData["name"] |
The nearest parent object or repeated row |
| Admin visibility condition | field.Sibling("name", ...) |
The nearest parent object or repeated row |
| Collection or field hook | ctx.Data["name"] |
Submitted root data; persisted update values are under ctx.Original.Values |
Values use Ridu’s typed store.Value vocabulary. For example,
ctx.SiblingData["membersOnly"].BooleanValue() returns the boolean and whether the stored value is
actually a boolean; StringValue(), NumberValue(), ObjectValue(), and Values() handle the
other shapes.
See Conditional fields for field.Sibling paths, operators,
and compound presentation rules; Virtual fields for deriving
response values; and field access for row-scoped
authorization. Reading a sibling does not itself enforce a rule: conditions are presentation,
while access and hooks run on the server.
Nested fields
Use Group for one known object, Array for ordered rows with one shape, and Blocks for an ordered union. Named Tabs also create an object; unnamed tabs affect only presentation.
field.Group("seo", field.Fields(
field.Text("title"),
field.Textarea("description"),
)),
field.Array("links", field.Fields(
field.Text("label", field.Required()),
field.Text("url", field.Required()),
), field.MaxRows(12)),Nested fields generate typed contracts, validate precise paths, support migrations and queries, and render structured controls. Prefer them over JSON when the shape is known.
Authoring layout
Rows, collapsibles, unnamed tabs, and UI fields organise authors without nesting stored values.
Named tabs and Groups do create a stored boundary. ReadOnly and Hidden are also presentation,
not authorization.
field.Row(
field.Text("firstName", field.Columns(6)),
field.Text("lastName", field.Columns(6)),
),
field.Collapsible("advanced", true,
field.JSON("providerMetadata"),
),The stored paths remain firstName, lastName, and providerMetadata.
Relationships
Relationship stores one/many references to one or several collection targets. Upload is the focused reference to one upload-enabled collection. Option filters narrow author choices and are revalidated on the server, but target read access is still required. Population replaces stored references with access-checked target documents only when requested.
Use Join for an inverse view rather than keeping two ID lists synchronized. See Relationships, joins, and population for cardinality, delete behavior, atomic join mutation, and read limits.
Upload and rich-text fields
An Upload field references a media document; it does not accept raw bytes. Create the media through the admin, multipart REST, or generated SDK upload methods. The official Rich-text plugin uses a Plugin field with a portable versioned document, Go validation, generated types, and a statically paired Svelte/Lexical editor.
Joins and computed fields
Join and Virtual values appear in generated output but never mutation
input. A Join reads inverse relationships through target access. A Virtual calls a matching trusted
Computed resolver and validates its result against ValueString, ValueNumber, ValueBoolean,
or ValueJSON. Both run only when selected output needs them.
Localization
Localized requires application content locales. Put it on one scalar/reference when only that
value varies, or on Group/Array/Blocks when the complete structure varies per locale. Container
localization prevents fallback from mixing descendant values from different locale objects.
Admin-interface translations for labels, descriptions, choices, blocks, tabs, and row names use
their *Translations metadata and are independent from stored content localization. See
Content localization.
Field access and hooks
Collection.FieldAccess maps canonical paths to executable read/write decisions;
Collection.FieldHooks maps paths to lifecycle hooks. Presentation never substitutes for either.
Filtered collection access remains in the atomic store query, while field read rules redact denied
values after a document is admitted.
Use BeforeValidate to normalize, BeforeChange for transaction-bound derived stored values, and
AfterChange only when an external effect can safely be retried. Continue with
Access control and Hooks.
Evolve a field safely
Adding an optional field is usually additive. Renames, kind/cardinality changes, removed choices or
block types, narrowed relationship targets, new required constraints, and removed fields can need a
data transform or coordinated migration. Run ridu migrate create, review the artifact, verify it
against an isolated target, and rehearse production data before applying it. Never use development
sync as a substitute for a deployment plan.
Modelling patterns
| Need | Prefer |
|---|---|
| Short or multi-line plain text | Text or Textarea |
| Formatted portable document | Official Rich text plugin |
| One known object / repeated one-shape rows / mixed rows | Group / Array / Blocks |
| Fixed values / managed records | Select / Relationship |
| Media reference | Upload to an upload-enabled collection |
| Editor organisation without API nesting | Row, Collapsible, unnamed Tabs, Columns |
| Derived response value | Root Virtual plus Computed resolver |
| Reusable new value kind | Compiled Plugin field with exact generated/admin pairing |
Prefer the narrowest shape that represents the domain. See the individual field pages for behavior
and the field API reference for signatures.