Webiny-js webiny-api-cms-custom-field-type
install
source · Clone the upstream repo
git clone https://github.com/webiny/webiny-js
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/webiny/webiny-js "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/user-skills/api/custom-field-type" ~/.claude/skills/webiny-webiny-js-webiny-api-cms-custom-field-type && rm -rf "$T"
manifest:
skills/user-skills/api/custom-field-type/SKILL.mdsource content
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()
) infields.color()
implementationsModelFactory
Field Type Structure
A custom field type consists of three parts:
- Builder interface — extends
plusDataFieldBuilder<"type">
typesFieldTypeValidator.* - Builder class — implements the interface, calls
for each validatorthis.validation() - Factory class — implements
, creates builder instancesFieldType.Factory
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 |
|---|---|
| Field label shown in the Admin editor |
| Help text shown below the field |
| Field description |
| Override the auto-derived field ID |
| Override the storage identifier |
| Placeholder text for the input |
| Default value for new entries |
| Make the field accept multiple values (array) |
| Minimum number of list items |
| Maximum number of list items |
| Arbitrary tags for filtering/querying |
| Set the Admin UI renderer |
| Set arbitrary field settings |
Protected Methods (for use inside validator implementations only)
| Method | Description |
|---|---|
| Append a to the field's validation array |
| Append a 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 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
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 | | |
|---|---|---|
| Required, Unique | / | |
| MinLength, MaxLength | / | |
| Gte, Lte | / | |
| DateGte, DateLte | / | |
| Pattern | | |
| | |
| Url | | |
| LowerCase / UpperCase / etc. | | / / etc., |
Key Rules
string must be unique — the factory'stype
must not collide with any built-in type (readonly type
,text
,number
,boolean
,datetime
,file
,ref
,object
,richText
,longText
,json
) or other custom types.dynamicZone- Module augmentation target — augment
using"webiny/api/cms/model"
.namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }
is protected — never call it from outside the builder class. Expose validators as named methods on the interface (e.g.,validation()
,required()
).minLength()
— field type factories have no DI dependencies; always pass an empty array.dependencies: []- Registration order — register custom
implementations beforeFieldType
is resolved (i.e., in the sameFieldBuilderRegistry
call or before it runs). The registry collects allregister()
instances at construction time.FieldType
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
,FieldTypeDataFieldBuilder - webiny-dependency-injection — The
pattern and DI scopingcreateImplementation