webiny-api-cms-custom-field-type
GitHub指导如何为Webiny CMS实现自定义字段类型,涵盖扩展DataFieldBuilder、创建工厂类、DI注册及TypeScript模块增强,以支持自定义存储格式和验证逻辑。
Trigger Scenarios
Install
npx skills add webiny/webiny-js --skill webiny-api-cms-custom-field-type -g -y
SKILL.md
Frontmatter
{
"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()) inModelFactoryimplementations
Field Type Structure
A custom field type consists of three parts:
- Builder interface — extends
DataFieldBuilder<"type">plusFieldTypeValidator.*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 } |
"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
typestring must be unique — the factory'sreadonly typemust 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"usingnamespace 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
FieldTypeimplementations beforeFieldBuilderRegistryis resolved (i.e., in the sameregister()call or before it runs). The registry collects allFieldTypeinstances 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
createImplementationpattern and DI scoping
Version History
- 80eb1c5 Current 2026-08-20 10:07


