---
title: Field Metadata
description: Configure field types and properties — 40+ field types for text, numbers, dates, relationships, and more
---

# Field Metadata

A **Field** defines an individual property within an Object. ObjectStack provides 40+ field types covering text, numbers, dates, selections, relationships, files, calculations, and specialized types like vectors and QR codes.

## Basic Usage

Fields are defined inside an Object's `fields` map using `Field.*` factory methods:

```typescript
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Contact = ObjectSchema.create({
  name: 'contact',
  label: 'Contact',
  fields: {
    first_name: Field.text({ label: 'First Name', required: true }),
    last_name: Field.text({ label: 'Last Name', required: true }),
    email: Field.email({ label: 'Email', unique: true }),
    phone: Field.phone({ label: 'Phone' }),
    birth_date: Field.date({ label: 'Date of Birth' }),
    is_active: Field.boolean({ label: 'Active', defaultValue: true }),
    account: Field.lookup('account', { label: 'Account', required: true }),
  },
});
```

## Field Types

### Text Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `text` | `Field.text()` | Single-line text |
| `textarea` | `Field.textarea()` | Multi-line text |
| `email` | `Field.email()` | Email address with validation |
| `url` | `Field.url()` | URL with validation |
| `phone` | `Field.phone()` | Phone number |
| `password` | `Field.password()` | Masked password input |
| `markdown` | `Field.markdown()` | Markdown editor |
| `html` | `Field.html()` | HTML content |
| `richtext` | `Field.richtext()` | Rich text (WYSIWYG) editor |

```typescript
name: Field.text({ label: 'Name', required: true, maxLength: 255 }),
bio: Field.textarea({ label: 'Bio', maxLength: 5000 }),
website: Field.url({ label: 'Website' }),
notes: Field.markdown({ label: 'Notes' }),
```

### Number Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `number` | `Field.number()` | Integer or decimal |
| `currency` | `Field.currency()` | Monetary value with currency support |
| `percent` | `Field.percent()` | Percentage value |

```typescript
quantity: Field.number({ label: 'Quantity', min: 0, max: 10000, step: 1 }),
price: Field.currency({ label: 'Price', scale: 2, min: 0 }),
discount: Field.percent({ label: 'Discount', scale: 2, min: 0, max: 100 }),
```

**Currency Configuration:**

```typescript
price: Field.currency({
  label: 'Price',
  currencyConfig: {
    precision: 2,
    currencyMode: 'dynamic',    // 'dynamic' | 'fixed'
    defaultCurrency: 'USD',
  },
}),
```

### Date & Time Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `date` | `Field.date()` | Date only |
| `datetime` | `Field.datetime()` | Date and time |
| `time` | — | Time only |

```typescript
start_date: Field.date({ label: 'Start Date', required: true }),
created_at: Field.datetime({ label: 'Created At', readonly: true }),
```

### Boolean Type

```typescript
is_active: Field.boolean({ label: 'Active', defaultValue: true }),
```

### Selection Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `select` | `Field.select()` | Single or multi-select dropdown |
| `radio` | — | Radio button group |
| `checkboxes` | — | Checkbox group |

```typescript
// Single select
status: Field.select({
  label: 'Status',
  options: [
    { label: 'New', value: 'new', color: '#999', default: true },
    { label: 'Active', value: 'active', color: '#0A0' },
    { label: 'Closed', value: 'closed', color: '#A00' },
  ],
}),

// Multi-select
skills: Field.select({
  label: 'Skills',
  multiple: true,
  options: [
    { label: 'JavaScript', value: 'js' },
    { label: 'Python', value: 'python' },
    { label: 'Go', value: 'go' },
  ],
}),
```

Options must use `{ label, value }` objects. Values are stored in the database and must be lowercase.

### Relationship Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `lookup` | `Field.lookup()` | Many-to-one reference |
| `master_detail` | `Field.masterDetail()` | Parent-child with cascade delete |

```typescript
// Simple lookup
account: Field.lookup('account', {
  label: 'Account',
  required: true,
}),

// Filtered lookup
primary_contact: Field.lookup('contact', {
  label: 'Primary Contact',
  referenceFilters: ['account = {account}', 'is_active = true'],
}),

// Master-detail (cascade delete)
project: Field.masterDetail('project', {
  label: 'Project',
  required: true,
}),

// Inline line items on the parent form
order: Field.masterDetail('order', {
  label: 'Order',
  required: true,
  inlineEdit: true,
  inlineTitle: 'Lines',
  inlineAmountField: 'line_total',
}),
```

| Property | Type | Description |
| :--- | :--- | :--- |
| `reference` (1st arg) | `string` | Target object name |
| `referenceFilters` | `string[]` | Filter conditions for the lookup |
| `deleteBehavior` | `enum` | `'set_null'`, `'cascade'`, `'restrict'` |
| `inlineEdit` | `boolean` | Render child records inline on the parent create/edit form |
| `inlineColumns` | `array` | Optional explicit columns for the inline grid |
| `inlineAmountField` | `string` | Optional numeric child field for the inline running total |

### File & Media Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `image` | `Field.image()` | Image file upload |
| `file` | `Field.file()` | Generic file upload |
| `avatar` | `Field.avatar()` | User avatar/profile image |
| `video` | — | Video file |
| `audio` | — | Audio file |

```typescript
photo: Field.image({ label: 'Photo' }),
resume: Field.file({ label: 'Resume' }),
profile_image: Field.avatar({ label: 'Profile Image' }),
```

### Calculation Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `formula` | `Field.formula()` | Calculated from other fields |
| `summary` | `Field.summary()` | Roll-up from child records |
| `autonumber` | `Field.autonumber()` | Sequential auto-generated number |

```typescript
// Formula
total: Field.formula({
  label: 'Total',
  expression: 'quantity * price * (1 - discount / 100)',
}),

// Summary (roll-up)
total_amount: Field.summary({
  label: 'Total Amount',
  summaryOperations: {
    object: 'order_item',
    field: 'amount',
    function: 'sum',
    relationshipField: 'order',
  },
}),

// AutoNumber
case_number: Field.autonumber({
  label: 'Case Number',
  autonumberFormat: 'CASE-{0000}',
}),
```

Summary fields are server-owned. The engine recomputes them when child records
are inserted, updated, or deleted. `relationshipField` is optional when the child
has only one `lookup`/`master_detail` field pointing back to the parent; set it
when multiple relationships target the same parent object.

### Specialized Types

| Type | Factory | Description |
| :--- | :--- | :--- |
| `address` | `Field.address()` | Structured address (street, city, state, country) |
| `location` | `Field.location()` | Geographic coordinates (lat/lng) |
| `color` | `Field.color()` | Color picker (hex/rgb/hsl) |
| `rating` | `Field.rating()` | Star rating (1-5) |
| `slider` | `Field.slider()` | Range slider |
| `signature` | `Field.signature()` | Digital signature capture |
| `qrcode` | `Field.qrcode()` | QR/barcode generator |
| `code` | `Field.code()` | Code editor with syntax highlighting |
| `json` | — | JSON data |
| `vector` | `Field.vector()` | AI embedding vectors |
| `tags` | — | Tag list |
| `progress` | — | Progress bar |

```typescript
office: Field.address({ label: 'Office Address', addressFormat: 'international' }),
coordinates: Field.location({ label: 'Location', displayMap: true }),
brand_color: Field.color({ label: 'Color', colorFormat: 'hex' }),
satisfaction: Field.rating({ label: 'Rating', maxRating: 5 }),
approval_signature: Field.signature({ label: 'Signature' }),
embedding: Field.vector({ label: 'Embedding', vectorConfig: { dimensions: 1536 } }),
```

## Common Properties

These properties are available on all field types:

### Constraints

| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `required` | `boolean` | `false` | Field must have a value |
| `unique` | `boolean` | `false` | Values must be unique across records |
| `defaultValue` | `any` | — | Default value for new records |
| `maxLength` | `number` | — | Maximum character length |
| `minLength` | `number` | — | Minimum character length |
| `min` | `number` | — | Minimum numeric value |
| `max` | `number` | — | Maximum numeric value |

### Display

| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `label` | `string` | — | Human-readable display name |
| `description` | `string` | — | Developer documentation |
| `inlineHelpText` | `string` | — | Help text shown in UI |
| `hidden` | `boolean` | `false` | Hide from default views |
| `readonly` | `boolean` | `false` | Prevent editing |
| `sortable` | `boolean` | `true` | Allow sorting by this field |
| `group` | `string` | — | Group name for organizing in forms (e.g. `'billing'`) |

### Search & Index

| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `searchable` | `boolean` | `false` | Include in full-text search |
| `index` | `boolean` | `false` | Create a database index |
| `externalId` | `boolean` | `false` | Mark as external system identifier |

### Security

| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `encryptionConfig` | `object` | — | Field-level encryption settings |
| `maskingRule` | `object` | — | Data masking rules for display |
| `auditTrail` | `boolean` | `false` | Track all changes to this field |

### Conditional Logic

| Property | Type | Description |
| :--- | :--- | :--- |
| `visibleWhen` | `string \| Expression` | CEL predicate; field is shown only when `TRUE` |
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
| `requiredWhen` | `string \| Expression` | CEL predicate; field is required when `TRUE` |
| `conditionalRequired` | `string \| Expression` | Deprecated alias of `requiredWhen` |
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |

```typescript
import { P } from '@objectstack/spec';
import { Field } from '@objectstack/spec/data';

close_reason: Field.select({
  label: 'Close Reason',
  visibleWhen: P`record.status == 'closed'`,
  requiredWhen: P`record.status == 'closed'`,
  options: [
    { label: 'Won', value: 'won' },
    { label: 'Lost', value: 'lost' },
    { label: 'Cancelled', value: 'cancelled' },
  ],
}),

invoice_total: Field.currency({
  label: 'Invoice Total',
  readonlyWhen: P`record.status == 'paid'`,
}),
```

Conditional field rules are evaluated in both tiers. ObjectUI forms update
visibility, read-only state, and required markers live as the draft record
changes, including row-scoped rules inside inline master-detail grids. The
server enforces `requiredWhen` on submit and ignores writes to fields whose
`readonlyWhen` predicate is `TRUE`, so client-side affordances are not the
only guard. Use `requiredWhen` for new metadata; keep `conditionalRequired`
only for older packages that already emitted it.

## Naming Conventions

- Field names use `snake_case`: `first_name`, `annual_revenue`, `is_active`
- Boolean fields use `is_` or `has_` prefix: `is_active`, `has_children`
- Lookup fields use the object name directly: `account` (not `account_id`)
- Configuration keys use `camelCase`: `maxLength`, `defaultValue`

## Related

- [Object Metadata](./object) — Define business entities
- [Validation Metadata](./validation) — Field-level and cross-field validation rules
- [View Metadata](./view) — Control how fields are displayed in views
