メインコンテンツまでスキップ

Data Modeling — Fields / Lexical / Relationships / Versions / Localization

この章のゴール

  • PayloadCMS の Field 体系 (Data / Presentational / Virtual の 3 区分) を把握する
  • リレーション系 (relationship / upload) と構造系 (array / group / tabs / blocks) の使い分けを掴む
  • Lexical Rich Text のデフォルト機能と feature の足し方を理解する
  • Localization (多言語化)Versions & Drafts の必要最小限の設定を書ける

Field が住む場所 (おさらい)

02 Core Concepts で見たとおり、PayloadCMS のデータモデルは 3 階層です:

Payload Config (payload.config.ts)
├─ Collections (繰り返し / posts, users, media …) → fields[] ← この章
└─ Globals (シングルトン / header, footer …) → fields[] ← この章

公式 doc は Globals を「many ways similar to Collections, except that they correspond to only a single Document」と定義しています (Globals)。繰り返しかシングルトンかという入れ物は違っても、中身を定義する fields 配列の語彙は共通で、この章で扱うのはその fields[] の世界です。

加えて group / array / blocks / tabs などのフィールド型は自身がさらに fields を持つため、ツリーは再帰的に深くなり得ます (Fields Overview)。詳細は本章の各セクションで扱います。

Field の 3 区分

公式 doc (Fields Overview) は Field を 3 つに分けて説明しています。

区分役割含まれる type
Data FieldsDB に値を保存するtext / textarea / email / number / date / select / checkbox / radio / array / blocks / group / tabs (named) / relationship / upload / richText / code / json / point
Presentational FieldsUI 用、DB には保存しないcollapsible / row / tabs (unnamed) / group (unnamed) / ui
Virtual Fields計算で導出する値join / 任意フィールドの virtual: true

「Presentational Fields は DB に値を保存せず、Admin Panel で他のフィールドを整理・表示するために使う」と公式 doc も明記しています。Data フィールドは name を必須にしますが、Presentational は name 不要、という違いで区別できます。

全 Field 共通プロパティ

プロパティ意味
nameDB のキー (Data Fields は必須)
typeフィールドの挙動を決定する (必須)
labelAdmin UI 表示名
requiredバリデーション (空を禁止)
validateカスタムバリデーション関数
hooksbeforeChange / afterRead など Field レベルのライフサイクル
accessField レベルのアクセス制御
adminUI カスタマイズ (description / position / readOnly 等)
defaultValue初期値
localizedtrue で多言語化 (後述)
{
name: 'myField',
type: 'text',
required: true,
admin: { description: 'A simple text field' },
}

プリミティブ Field カタログ

最小例とユースケースを並べます。

{ name: 'title', type: 'text' } // 1 行テキスト
{ name: 'body', type: 'textarea' } // 複数行テキスト
{ name: 'email', type: 'email' } // メール (バリデーション込)
{ name: 'price', type: 'number', min: 0 } // 数値
{ name: 'publishedAt', type: 'date' } // 日付
{ name: 'status', type: 'select', options: ['draft', 'published'] } // 列挙
{ name: 'isFeatured', type: 'checkbox' } // 真偽値
{ name: 'tier', type: 'radio', options: ['free', 'pro'] }
{ name: 'config', type: 'json' } // JSON 任意構造
{ name: 'snippet', type: 'code', admin: { language: 'ts' } }
{ name: 'location', type: 'point' } // 地理座標 [lng, lat]

リレーション系 Field

relationship

別の Collection を参照します。1:1、1:N、polymorphic (複数 Collection のうちどれか) も書けます。

{
name: 'author',
type: 'relationship',
relationTo: 'users', // 単一 Collection
required: true,
}

{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true, // 1:N
}

{
name: 'related',
type: 'relationship',
relationTo: ['posts', 'pages'], // polymorphic
}

Admin UI には関連 Collection を検索しながら選ぶピッカーが自動で現れ、API レスポンスは depth パラメータでネスト展開できます (REST: ?depth=2、Local: payload.find({ ..., depth: 2 }))。

polymorphic relationship の詳しい挙動

relationTo配列 (['users', 'organizations']) で渡すと polymorphic relationship になります。これは「型が違うものを 1 つの口で受ける」関係です。たとえば「お知らせの送信先」が User でも Organization でも構わない、あるいは「コメントの対象」が Post でも Page でもよい、というモデルを 1 つの field で表現できます。

DB 保存形式の違い

ここがポイントで、単一 Collection を指す relationship と polymorphic とでは、DB に保存される値の形が変わります。

// relationTo: 'users' (単一) のとき、値は ID 文字列 (or 数値)
{
"author": "6031ac9e1289176380734024"
}

// relationTo: ['users', 'organizations'] (polymorphic) のとき、
// 値は「どの Collection の ID か」が分かる object 形式
{
"owners": [
{ "relationTo": "users", "value": "6031ac9e1289176380734024" },
{ "relationTo": "organizations", "value": "602c3c327b811235943ee12b" }
]
}

polymorphic では 「ID 単体」では足りない ことに注目してください。「6031ac9e... という ID」だけでは、それが users のものか organizations のものか判別できないため、relationTo を併せて保存する必要があります。

公式の完全例

公式 doc が示す polymorphic + hasMany + required を全部組み合わせた例:

{
name: 'owners',
type: 'relationship',
relationTo: ['users', 'organizations'],
hasMany: true,
required: true,
}
フロントでの分岐パターン

取得側 (Server Component / API consumer) は relationTo の値で 分岐して描画します。depth: 0 だと value は ID のまま、depth: 1 以上で関連ドキュメント本体が展開される、という挙動は Queries — Depth に明示されています (公式の単一 relationship の例で示されている挙動を、polymorphic の { relationTo, value } 構造に当てはめると以下のような分岐になります):

// depth: 1 以上で取得した場合、value にドキュメント本体が入る
post.owners?.map((owner) => {
if (typeof owner.value === 'string') return null // 何らかの理由で展開されていない
if (owner.relationTo === 'users') {
return <UserCard key={owner.value.id} user={owner.value} />
}
if (owner.relationTo === 'organizations') {
return <OrgCard key={owner.value.id} org={owner.value} />
}
return null
})

@/payload-types から自動生成される型では valueUser | Organization | string のような union になり、owner.relationTo === 'users' の判定で TypeScript の型 narrowing が効くため、上記の分岐は型安全に書けます。

いつ使う / 避ける
  • 使う: 「同じスロットに 異なる種類のドキュメントが入りうる」とき。例: 通知の宛先 (User / Organization)、関連リンク (Post / Page)、コメントの対象 (Article / Product / Event)
  • 避ける: 1 種類しか想定しないとき。単一 relationTo: 'users' の方が 値の形がシンプル (string で済む) で、フロント側の typeof チェックや switch 分岐が要らないぶん、コードが軽くなります

迷ったら 「将来、複数 Collection を受ける可能性がゼロか?」 を自問し、ゼロなら単一、可能性があるなら polymorphic、で選びます。

出典: Relationship Field (公式 doc)

upload

Upload-enabled Collection (例: Media) への参照です。typically 画像・PDF を relationship 的に紐付けるための糖衣構文。

{
name: 'thumbnail',
type: 'upload',
relationTo: 'media',
}

Media Collection 側で upload: { staticDir, imageSizes } を設定すると、imageSizes 設定どおりに自動でリサイズ画像が生成されます (詳細は 07 Admin / Media / Deploy)。

構造系 Field

Field用途
groupフィールドをまとめて 1 つのオブジェクトに格納 ({ name: 'seo', type: 'group', fields: [...] })
array同一スキーマのサブドキュメントを N 個並べる
tabsAdmin UI 上でフィールドをタブ分割
blocks異なるスキーマのサブドキュメントを N 個並べる (★ページビルダーの心臓)

blocks — ページビルダーの心臓

公式 doc (Blocks) によれば、Blocks Field は「複数の異なるスキーマを持つオブジェクトの配列」を保存します。「ページビルダーで Quote、CallToAction、Slider、Gallery を混在させる」のが典型用途です。

import type { Block } from 'payload'

const Hero: Block = {
slug: 'Hero',
fields: [
{ name: 'headline', type: 'text' },
{ name: 'image', type: 'upload', relationTo: 'media' },
],
}

const CallToAction: Block = {
slug: 'CallToAction',
fields: [
{ name: 'label', type: 'text' },
{ name: 'href', type: 'text' },
],
}

// Collection の fields の中で使う
{
name: 'layout',
type: 'blocks',
blocks: [Hero, CallToAction],
}

Admin UI には「+ Add Block」ボタンが出て、編集者は Hero と CallToAction を順番に積み上げてページを組めます。コードベース側は「どのブロックがどう描画されるか」を 1 対 1 でフロントに用意するだけで、CMS の入稿フローと完全に分離できます。

Array との違い (公式の説明): array は「全項目が同一スキーマ」、blocks は「異なるコンテンツタイプを混在させ任意順序で並べる」。

Lexical Rich Text

立ち位置

3.x で Lexical (Meta が開発) が デフォルトかつ stable な Rich Text Editor になりました。旧 Slate adapter は v4.0 で削除予定 と公式 doc (Rich Text Overview) に明記されており、新規プロジェクトは Lexical を選ぶのが既定路線です。Slate からの移行ガイドも公式で提供されています。

デフォルト features

Lexical エディタは feature の集合で構成されます。公式 doc (Official Features) が「Included by default: Yes」と明示している主な feature を整理すると:

  • HeadingFeature — 見出し H1〜H6
  • UnorderedListFeature / OrderedListFeature / ChecklistFeature — 箇条書き / 番号付き / チェックリスト
  • BlockquoteFeature — 引用ブロック
  • LinkFeature — ハイパーリンク
  • UploadFeature — エディタ内に画像 / PDF 等を埋め込み
  • RelationshipFeature — エディタ内に Collection ドキュメントを埋め込み
  • HorizontalRuleFeature — 区切り線
  • ほか Bold / Italic / Underline などのインライン書式、Align / Indent

一方 BlocksFeature (エディタ内に Blocks Field と同じ Block を埋め込む、コンテンツの中にコンテンツ) はデフォルトに含まれません ("Included by default: No")。次の「features の追加」のように明示的に加えます。

features の追加

editor プロパティで feature を組み立てます。

import { lexicalEditor, BlocksFeature, LinkFeature } from '@payloadcms/richtext-lexical'

// Collection field 内で
{
name: 'body',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({ blocks: [Hero, CallToAction] }),
LinkFeature({ fields: [{ name: 'utm', type: 'text' }] }),
],
}),
}

defaultFeatures を spread して追加・差し替えする形が公式の推奨パターンです。

フロント描画

Lexical の出力は JSON 構造で保存され、フロント側は公式提供の RichText コンポーネントで React 要素に変換します (具体的な実装は 06 章 6.10 節 で扱います)。

Localization (多言語化)

Config レベル

// payload.config.ts
export default buildConfig({
// ...
localization: {
locales: [
{ label: 'English', code: 'en' },
{ label: 'Arabic', code: 'ar', rtl: true },
{ label: '日本語', code: 'ja' },
],
defaultLocale: 'en',
fallback: true,
},
})

locales は文字列配列 (['en', 'es', 'de']) でも OK、fallback: true (デフォルト) でロケール未入力時に defaultLocale の値を返します。

Field レベル

localized: true を付けたフィールドは ロケールごとのオブジェクトとして DB に保存されます。

{ name: 'title', type: 'text', localized: true }

Admin UI には言語切替セレクタが現れ、編集者はロケール別に値を入力できます。

API でのロケール指定

API書き方
RESTGET /api/posts?locale=ja&fallback-locale=none
GraphQLPosts(locale: ja, fallbackLocale: none)
Localpayload.find({ collection: 'posts', locale: 'ja', fallbackLocale: false })

Versions & Drafts

Versions の有効化

Collection に versions: true を付けるとドキュメントの履歴が保存されます。

{
slug: 'posts',
versions: {
drafts: true,
maxPerDoc: 100, // デフォルト 100、超えると古いものから消える
},
fields: [/* ... */],
}

drafts: true を有効にすると、各ドキュメントに _status: 'draft' | 'published' フィールドが追加され、公開版より新しい draft を持てるようになります (公式 doc Versions Overview)。Access Control で「未公開 draft の閲覧は author 本人と admin だけ」のような制御ができます。

Autosave

versions: {
drafts: {
autosave: true,
},
}

Autosave を有効にすると、Admin UI で編集中に自動で 新しい draft 版が保存されます。既存の published 版には影響せず、編集者が「Publish」を押すまで公開には反映されません。

Live Preview / Draft Preview との関係

  • Live Preview: Admin UI で書きながら同じ画面でフロントが即更新される (iframe + postMessage)
  • Draft Preview: ?preview=... のような限定 URL で未公開 draft をフロントに表示する

Live Preview は Versions と独立した機能、Draft Preview は Versions の drafts: true が前提です。両方とも 06 Next.js Integration で詳しく扱います。

実装例 — Blocks の選択肢を他フィールドで絞る (filterOptions)

ページビルダーで「特定の条件下でのみ選択できる Block」を表現したいことがあります。たとえば「公開モードが drafts のときは Hero ブロックを出さない」「テンプレートを Article に切り替えたら使える Block セットを差し替える」など。

Blocks Field の filterOptions を使うと、同じドキュメント内の他フィールドの値に応じて表示する Block を動的に絞れます (Blocks Field doc)。

{
name: 'layout',
type: 'blocks',
blocks: [Hero, RichText, CallToAction],
filterOptions: ({ siblingData }) => {
return siblingData?.enabledBlocks?.length
? [siblingData.enabledBlocks]
: true
},
}
  • 返り値 true → 定義した全 Block を許可
  • 返り値が block slugs の配列 → その slug の Block だけ許可

シナリオ例: enabledBlocks という select フィールドを同じ Collection に置き、編集者がそこで「使う Block セット」を選ぶと、Blocks Field の Admin UI が動的にフィルタされる、という流れです。

出典: Blocks Field (公式 doc)

この章のまとめ

  • Field は Data (DB に保存) / Presentational (UI 用) / Virtual (導出) の 3 区分
  • リレーション系は relationship (1:1/1:N/polymorphic) と upload (Media 参照糖衣構文)
  • 構造系の主役は blocks — 異なるスキーマを順不同で並べるページビルダーの心臓
  • Rich Text は Lexical が標準・stable、Slate は v4.0 で削除予定
  • Localization は config の localization + field の localized: true の 2 階建て
  • Versions の drafts: true で公開/下書きが分離、autosave: true で編集中の自動保存
  • Blocks Field は filterOptions で他フィールド値に応じて選択肢を動的に制御できる

次に読む

次章 04 Security & Lifecycle — Auth / Access Control / Hooks では、認証・認可・Hooks のライフサイクルを扱います。データを定義したあと「誰が読めて、誰が書けて、書き換えた瞬間に何が起きるか」のレイヤです。

参考