Custom CMS Field Type
TL;DR
A custom field type is a class that extends DataFieldBuilder<"yourType">, paired with a factory class implementing FieldType.Factory. Register the factory with container.register(YourFieldType). Add a module augmentation on "webiny/api/cms/model" so the fields registry method gets TypeScript autocomplete.
When to Use This
Use a custom field type when:
- You need a field with a storage format or validation logic not covered by the built-in types (
text, number, boolean, datetime, file, ref, object, richText, longText, json, dynamicZone)
- You want to expose a fluent builder API (e.g.,
fields.slug(), fields.color()) in ModelFactory implementations
Field Type Structure
A custom field type consists of three parts:
- Builder interface — extends
DataFieldBuilder<"type"> plus FieldTypeValidator.* types
- Builder class — implements the interface, calls
this.validation() for each validator
- Factory class — implements
FieldType.Factory, creates builder instances
As a standalone extension (not part of a larger feature), the directory layout is:
extensions/
└── SlugFieldType/
├── SlugFieldType.ts # builder interface, builder class, factory class
└── feature.ts # createFeature — registers the factory into the DI container
feature.ts:
// extensions/SlugFieldType/feature.ts
import { createFeature } from "webiny/api";
import { SlugFieldType } from "./SlugFieldType.js";
export const SlugFieldTypeFeature = createFeature({
name: "SlugFieldType",
register(container) {
container.register(SlugFieldType);
}
});
Register in the API entry point:
// api/Extension.ts
import { createFeature } from "webiny/api";
import { SlugFieldTypeFeature } from "~/extensions/SlugFieldType/feature.js";
export const Extension = createFeature({
name: "MyExtension",
register(container) {
SlugFieldTypeFeature.register(container);
}
});
Complete Example
// extensions/SlugFieldType/SlugFieldType.ts
import { DataFieldBuilder, FieldType } from "webiny/api/cms/model";
import type { FieldTypeValidator } from "webiny/api/cms/model";
// 1. Builder interface — extends DataFieldBuilder + desired FieldTypeValidator types
export interface ISlugFieldBuilder
extends
DataFieldBuilder<"slug">,
FieldTypeValidator.Required,
FieldTypeValidator.Pattern,
FieldTypeValidator.Unique {}
// 2. Module augmentation — adds fields.slug() to the registry
declare module "webiny/api/cms/model" {
interface IFieldBuilderRegistry {
slug(): ISlugFieldBuilder;
}
interface IFieldRendererRegistry {
myCustomRenderer: {
fieldType: "text" | "number";
settings: undefined;
};
}
}
// 3. Builder class — implements each validator method via this.validation()
class SlugFieldBuilder extends DataFieldBuilder<"slug"> implements ISlugFieldBuilder {
constructor() {
super("slug");
}
required(message?: string): this {
return this.validation({
name: "required",
message: message || "Value is required.",
settings: {}
});
}
pattern(regex: string, flags = "", message?: string): this {
return this.validation({
name: "pattern",
message: message || "Invalid value.",
settings: { preset: "custom", regex, flags }
});
}
unique(message?: string): this {
return this.validation({
name: "unique",
message: message || "Value must be unique.",
settings: {}
});
}
}
// 4. Factory class — implements FieldType.Factory
class SlugFieldTypeFactory implements FieldType.Factory {
readonly type = "slug";
create(): ISlugFieldBuilder {
return new SlugFieldBuilder();
}
}
// 5. Export as a FieldType implementation
export const SlugFieldType = FieldType.createImplementation({
implementation: SlugFieldTypeFactory,
dependencies: []
});
Using the Custom Field in a Model
After registration, fields.slug() is available in any ModelFactory implementation:
import { ModelFactory } from "webiny/api/cms/model";
class ProductModelImpl implements ModelFactory.Interface {
async execute(builder: ModelFactory.Builder) {
return [
builder
.public({ modelId: "product", name: "Product", group: "ungrouped" })
.fields(fields => ({
name: fields.text().label("Name").required(),
slug: fields
.slug()
.label("Slug")
.required("Slug is required.")
.unique()
.pattern("^[a-z0-9-]+$", "", "Only lowercase letters, numbers, and hyphens.")
}))
.layout([["name", "slug"]])
.titleFieldId("name")
.singularApiName("Product")
.pluralApiName("Products")
];
}
}
DataFieldBuilder API
All methods return this for chaining.
| Method |
Description |
label(text) |
Field label shown in the Admin editor |
help(text) |
Help text shown below the field |
description(text) |
Field description |
fieldId(id) |
Override the auto-derived field ID |
storageId(id) |
Override the storage identifier |
placeholder(text) |
Placeholder text for the input |
defaultValue(value) |
Default value for new entries |
list() |
Make the field accept multiple values (array) |
listMinLength(n, msg?) |
Minimum number of list items |
listMaxLength(n, msg?) |
Maximum number of list items |
tags(tags) |
Arbitrary tags for filtering/querying |
renderer(name, settings?) |
Set the Admin UI renderer |
settings(settings) |
Set arbitrary field settings |
Protected Methods (for use inside validator implementations only)
| Method |
Description |
this.validation(rule) |
Append a CmsModelFieldValidation to the field's validation array |
this.listValidation(rule) |
Append a CmsModelFieldValidation to the list validation array |
A CmsModelFieldValidation has the shape:
{
name: string; // validator name (e.g., "required", "minLength", "pattern")
message: string; // error message shown to the user
settings: Record<string, any>; // validator-specific config
}
Available Validators
Import via import type { FieldTypeValidator } from "webiny/api/cms/model" and extend your builder interface with them. Each type adds one method to your interface:
| Type |
Method signature |
FieldTypeValidator.Required |
required(message?) |
FieldTypeValidator.Unique |
unique(message?) |
FieldTypeValidator.MinLength |
minLength(value, message?) |
FieldTypeValidator.MaxLength |
maxLength(value, message?) |
FieldTypeValidator.Pattern |
pattern(regex, flags?, message?) |
FieldTypeValidator.Email |
email(message?) |
FieldTypeValidator.Url |
url(message?) |
FieldTypeValidator.LowerCase |
lowerCase(message?) |
FieldTypeValidator.UpperCase |
upperCase(message?) |
FieldTypeValidator.LowerCaseSpace |
lowerCaseSpace(message?) |
FieldTypeValidator.UpperCaseSpace |
upperCaseSpace(message?) |
FieldTypeValidator.Gte |
gte(value, message?) |
FieldTypeValidator.Lte |
lte(value, message?) |
FieldTypeValidator.DateGte |
dateGte(value, message?) |
FieldTypeValidator.DateLte |
dateLte(value, message?) |
FieldTypeValidator.ListMinLength |
listMinLength(value, message?) |
FieldTypeValidator.ListMaxLength |
listMaxLength(value, message?) |
When implementing a validator method in the builder class, call this.validation() with the appropriate name and settings. For ListMinLength/ListMaxLength, call this.listValidation() instead. The settings shapes:
| Validator |
name |
settings |
| Required, Unique |
"required" / "unique" |
{} |
| MinLength, MaxLength |
"minLength" / "maxLength" |
{ value: String(n) } |
| Gte, Lte |
"gte" / "lte" |
{ value: String(n) } |
| DateGte, DateLte |
"dateGte" / "dateLte" |
{ value } |
| Pattern |
"pattern" |
{ preset: "custom", regex, flags } |
| Email |
"pattern" |
{ preset: "email", regex: null, flags: null } |
| Url |
"pattern" |
{ preset: "url", regex: null, flags: null } |
| LowerCase / UpperCase / etc. |
"pattern" |
{ preset: "lowerCase" / "upperCase" / etc., regex: null, flags: null } |
Key Rules
type string must be unique — the factory's readonly type must not collide with any built-in type (text, number, boolean, datetime, file, ref, object, richText, longText, json, dynamicZone) or other custom types.
- Module augmentation target — augment
"webiny/api/cms/model" using namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }.
validation() is protected — never call it from outside the builder class. Expose validators as named methods on the interface (e.g., required(), minLength()).
dependencies: [] — field type factories have no DI dependencies; always pass an empty array.
- Registration order — register custom
FieldType implementations before FieldBuilderRegistry is resolved (i.e., in the same register() call or before it runs). The registry collects all FieldType instances at construction time.
Related Skills
- webiny-api-cms-content-models — Using the model builder's fluent API to define CMS models
- webiny-api-cms-catalog — Full catalog of CMS abstractions including
ModelFactory, FieldType, DataFieldBuilder
- webiny-dependency-injection — The
createImplementation pattern and DI scoping
1---2name: webiny-api-cms-custom-field-type3description: How to implement a custom CMS field type that integrates with the model builder's fluent API. Covers extending DataFieldBuilder, composing validator interfaces, creating a FieldTypeFactory, registering via DI, and module augmentation for TypeScript autocomplete on the fields() registry.4---5
6# Custom CMS Field Type
7
8## TL;DR
9
10A custom field type is a class that extends `DataFieldBuilder<"yourType">`, paired with a factory class implementing `FieldType.Factory`. Register the factory with `container.register(YourFieldType)`. Add a module augmentation on `"webiny/api/cms/model"` so the `fields` registry method gets TypeScript autocomplete.
11
12## When to Use This
13
14Use a custom field type when:
15
16- You need a field with a storage format or validation logic not covered by the built-in types (`text`, `number`, `boolean`, `datetime`, `file`, `ref`, `object`, `richText`, `longText`, `json`, `dynamicZone`)
17- You want to expose a fluent builder API (e.g., `fields.slug()`, `fields.color()`) in `ModelFactory` implementations
18
19## Field Type Structure
20
21A custom field type consists of three parts:
22
231. **Builder interface** — extends `DataFieldBuilder<"type">` plus `FieldTypeValidator.*` types
242. **Builder class** — implements the interface, calls `this.validation()` for each validator
253. **Factory class** — implements `FieldType.Factory`, creates builder instances
26
27As a standalone extension (not part of a larger feature), the directory layout is:
28
29```
30extensions/
31└── SlugFieldType/
32 ├── SlugFieldType.ts # builder interface, builder class, factory class
33 └── feature.ts # createFeature — registers the factory into the DI container
34```
35
36`feature.ts`:
37
38```ts
39// extensions/SlugFieldType/feature.ts
40import { createFeature } from "webiny/api";
41import { SlugFieldType } from "./SlugFieldType.js";
42
43export const SlugFieldTypeFeature = createFeature({
44 name: "SlugFieldType",
45 register(container) {
46 container.register(SlugFieldType);
47 }
48});
49```
50
51Register in the API entry point:
52
53```ts
54// api/Extension.ts
55import { createFeature } from "webiny/api";
56import { SlugFieldTypeFeature } from "~/extensions/SlugFieldType/feature.js";
57
58export const Extension = createFeature({
59 name: "MyExtension",
60 register(container) {
61 SlugFieldTypeFeature.register(container);
62 }
63});
64```
65
66## Complete Example
67
68```ts
69// extensions/SlugFieldType/SlugFieldType.ts
70import { DataFieldBuilder, FieldType } from "webiny/api/cms/model";
71import type { FieldTypeValidator } from "webiny/api/cms/model";
72
73// 1. Builder interface — extends DataFieldBuilder + desired FieldTypeValidator types
74export interface ISlugFieldBuilder
75 extends
76 DataFieldBuilder<"slug">,
77 FieldTypeValidator.Required,
78 FieldTypeValidator.Pattern,
79 FieldTypeValidator.Unique {}
80
81// 2. Module augmentation — adds fields.slug() to the registry
82declare module "webiny/api/cms/model" {
83 interface IFieldBuilderRegistry {
84 slug(): ISlugFieldBuilder;
85 }
86
87 interface IFieldRendererRegistry {
88 myCustomRenderer: {
89 fieldType: "text" | "number";
90 settings: undefined;
91 };
92 }
93}
94
95// 3. Builder class — implements each validator method via this.validation()
96class SlugFieldBuilder extends DataFieldBuilder<"slug"> implements ISlugFieldBuilder {
97 constructor() {
98 super("slug");
99 }
100
101 required(message?: string): this {
102 return this.validation({
103 name: "required",
104 message: message || "Value is required.",
105 settings: {}
106 });
107 }
108
109 pattern(regex: string, flags = "", message?: string): this {
110 return this.validation({
111 name: "pattern",
112 message: message || "Invalid value.",
113 settings: { preset: "custom", regex, flags }
114 });
115 }
116
117 unique(message?: string): this {
118 return this.validation({
119 name: "unique",
120 message: message || "Value must be unique.",
121 settings: {}
122 });
123 }
124}
125
126// 4. Factory class — implements FieldType.Factory
127class SlugFieldTypeFactory implements FieldType.Factory {
128 readonly type = "slug";
129
130 create(): ISlugFieldBuilder {
131 return new SlugFieldBuilder();
132 }
133}
134
135// 5. Export as a FieldType implementation
136export const SlugFieldType = FieldType.createImplementation({
137 implementation: SlugFieldTypeFactory,
138 dependencies: []
139});
140```
141
142## Using the Custom Field in a Model
143
144After registration, `fields.slug()` is available in any `ModelFactory` implementation:
145
146```ts
147import { ModelFactory } from "webiny/api/cms/model";
148
149class ProductModelImpl implements ModelFactory.Interface {
150 async execute(builder: ModelFactory.Builder) {
151 return [
152 builder
153 .public({ modelId: "product", name: "Product", group: "ungrouped" })
154 .fields(fields => ({
155 name: fields.text().label("Name").required(),
156 slug: fields
157 .slug()
158 .label("Slug")
159 .required("Slug is required.")
160 .unique()
161 .pattern("^[a-z0-9-]+$", "", "Only lowercase letters, numbers, and hyphens.")
162 }))
163 .layout([["name", "slug"]])
164 .titleFieldId("name")
165 .singularApiName("Product")
166 .pluralApiName("Products")
167 ];
168 }
169}
170```
171
172## DataFieldBuilder API
173
174All methods return `this` for chaining.
175
176| Method | Description |
177| --------------------------- | --------------------------------------------- |
178| `label(text)` | Field label shown in the Admin editor |
179| `help(text)` | Help text shown below the field |
180| `description(text)` | Field description |
181| `fieldId(id)` | Override the auto-derived field ID |
182| `storageId(id)` | Override the storage identifier |
183| `placeholder(text)` | Placeholder text for the input |
184| `defaultValue(value)` | Default value for new entries |
185| `list()` | Make the field accept multiple values (array) |
186| `listMinLength(n, msg?)` | Minimum number of list items |
187| `listMaxLength(n, msg?)` | Maximum number of list items |
188| `tags(tags)` | Arbitrary tags for filtering/querying |
189| `renderer(name, settings?)` | Set the Admin UI renderer |
190| `settings(settings)` | Set arbitrary field settings |
191
192### Protected Methods (for use inside validator implementations only)
193
194| Method | Description |
195| --------------------------- | ------------------------------------------------------------------ |
196| `this.validation(rule)` | Append a `CmsModelFieldValidation` to the field's validation array |
197| `this.listValidation(rule)` | Append a `CmsModelFieldValidation` to the list validation array |
198
199A `CmsModelFieldValidation` has the shape:
200
201```ts
202{
203 name: string; // validator name (e.g., "required", "minLength", "pattern")
204 message: string; // error message shown to the user
205 settings: Record<string, any>; // validator-specific config
206}
207```
208
209## Available Validators
210
211Import via `import type { FieldTypeValidator } from "webiny/api/cms/model"` and extend your builder interface with them. Each type adds one method to your interface:
212
213| Type | Method signature |
214| ----------------------------------- | ---------------------------------- |
215| `FieldTypeValidator.Required` | `required(message?)` |
216| `FieldTypeValidator.Unique` | `unique(message?)` |
217| `FieldTypeValidator.MinLength` | `minLength(value, message?)` |
218| `FieldTypeValidator.MaxLength` | `maxLength(value, message?)` |
219| `FieldTypeValidator.Pattern` | `pattern(regex, flags?, message?)` |
220| `FieldTypeValidator.Email` | `email(message?)` |
221| `FieldTypeValidator.Url` | `url(message?)` |
222| `FieldTypeValidator.LowerCase` | `lowerCase(message?)` |
223| `FieldTypeValidator.UpperCase` | `upperCase(message?)` |
224| `FieldTypeValidator.LowerCaseSpace` | `lowerCaseSpace(message?)` |
225| `FieldTypeValidator.UpperCaseSpace` | `upperCaseSpace(message?)` |
226| `FieldTypeValidator.Gte` | `gte(value, message?)` |
227| `FieldTypeValidator.Lte` | `lte(value, message?)` |
228| `FieldTypeValidator.DateGte` | `dateGte(value, message?)` |
229| `FieldTypeValidator.DateLte` | `dateLte(value, message?)` |
230| `FieldTypeValidator.ListMinLength` | `listMinLength(value, message?)` |
231| `FieldTypeValidator.ListMaxLength` | `listMaxLength(value, message?)` |
232
233When implementing a validator method in the builder class, call `this.validation()` with the appropriate `name` and `settings`. For `ListMinLength`/`ListMaxLength`, call `this.listValidation()` instead. The `settings` shapes:
234
235| Validator | `name` | `settings` |
236| ---------------------------- | ----------------------------- | ---------------------------------------------------------------------------- |
237| Required, Unique | `"required"` / `"unique"` | `{}` |
238| MinLength, MaxLength | `"minLength"` / `"maxLength"` | `{ value: String(n) }` |
239| Gte, Lte | `"gte"` / `"lte"` | `{ value: String(n) }` |
240| DateGte, DateLte | `"dateGte"` / `"dateLte"` | `{ value }` |
241| Pattern | `"pattern"` | `{ preset: "custom", regex, flags }` |
242| Email | `"pattern"` | `{ preset: "email", regex: null, flags: null }` |
243| Url | `"pattern"` | `{ preset: "url", regex: null, flags: null }` |
244| LowerCase / UpperCase / etc. | `"pattern"` | `{ preset: "lowerCase"` / `"upperCase"` / etc., `regex: null, flags: null }` |
245
246## Key Rules
247
2481. **`type` string must be unique** — the factory's `readonly type` must not collide with any built-in type (`text`, `number`, `boolean`, `datetime`, `file`, `ref`, `object`, `richText`, `longText`, `json`, `dynamicZone`) or other custom types.
2492. **Module augmentation target** — augment `"webiny/api/cms/model"` using `namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }`.
2503. **`validation()` is protected** — never call it from outside the builder class. Expose validators as named methods on the interface (e.g., `required()`, `minLength()`).
2514. **`dependencies: []`** — field type factories have no DI dependencies; always pass an empty array.
2525. **Registration order** — register custom `FieldType` implementations before `FieldBuilderRegistry` is resolved (i.e., in the same `register()` call or before it runs). The registry collects all `FieldType` instances at construction time.
253
254## Related Skills
255
256- **webiny-api-cms-content-models** — Using the model builder's fluent API to define CMS models
257- **webiny-api-cms-catalog** — Full catalog of CMS abstractions including `ModelFactory`, `FieldType`, `DataFieldBuilder`
258- **webiny-dependency-injection** — The `createImplementation` pattern and DI scoping