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.md
source 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()
    ,
    fields.color()
    ) in
    ModelFactory
    implementations

Field Type Structure

A custom field type consists of three parts:

  1. Builder interface — extends
    DataFieldBuilder<"type">
    plus
    FieldTypeValidator.*
    types
  2. Builder class — implements the interface, calls
    this.validation()
    for each validator
  3. 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.

MethodDescription
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)

MethodDescription
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:

TypeMethod 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

  1. 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.
  2. Module augmentation target — augment
    "webiny/api/cms/model"
    using
    namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }
    .
  3. validation()
    is protected
    — never call it from outside the builder class. Expose validators as named methods on the interface (e.g.,
    required()
    ,
    minLength()
    ).
  4. dependencies: []
    — field type factories have no DI dependencies; always pass an empty array.
  5. 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