| name | webiny-api-cms-custom-field-type |
| description | 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.
|
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:
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:
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
import { DataFieldBuilder, FieldType } from "webiny/api/cms/model";
import type { FieldTypeValidator } from "webiny/api/cms/model";
export interface ISlugFieldBuilder
extends
DataFieldBuilder<"slug">,
FieldTypeValidator.Required,
FieldTypeValidator.Pattern,
FieldTypeValidator.Unique {}
declare module "webiny/api/cms/model" {
interface IFieldBuilderRegistry {
slug(): ISlugFieldBuilder;
}
interface IFieldRendererRegistry {
myCustomRenderer: {
fieldType: "text" | "number";
settings: undefined;
};
}
}
class SlugFieldBuilder extends DataFieldBuilder<"slug"> implements {
() {
();
}
(?: ): {
.({
: ,
: message || ,
: {}
});
}
(: , flags = , ?: ): {
.({
: ,
: message || ,
: { : , regex, flags }
});
}
(?: ): {
.({
: ,
: message || ,
: {}
});
}
}
. {
= ;
(): {
();
}
}
= .({
: ,
: []
});
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()
];
}
}
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;
message: string;
settings: Record<string, any>;
}
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