Creating a Custom Field Type in Drupal with the Field APIfor Drupal 11 , and 10

Last updated :  

Every field you've ever clicked "Add field" for in Drupal — text, number, image, date — is a plugin. Someone wrote a small PHP class, told Drupal about it with an annotation, and from that moment on it showed up in that big "Choose a type of field" grid you see when adding a field to a content type. In this lesson you'll read the class that does exactly that for a brand new field type: one that stores an RGB color value.

What you'll learn in this lesson

  • How Drupal discovers field type plugins using the @FieldType annotation
  • The three methods every field type plugin needs to implement, and what each one is responsible for
  • The difference between a field's database storage (schema()) and its logical data structure (propertyDefinitions()) — two different concerns that are easy to conflate as a beginner
  • Why a field type alone isn't enough to make a usable field — and what other two plugin types it always needs a partner from

The source file

Path: modules/field_example/src/Plugin/Field/FieldType/RgbItem.php

<?php

namespace Drupal\field_example\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;

/**
 * Plugin implementation of the 'field_example_rgb' field type.
 *
 * @FieldType(
 *   id = "field_example_rgb",
 *   label = @Translation("Example Color RGB"),
 *   module = "field_example",
 *   description = @Translation("Demonstrates a field composed of an RGB color."),
 *   default_widget = "field_example_text",
 *   default_formatter = "field_example_simple_text"
 * )
 */
class RgbItem extends FieldItemBase {

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'type' => 'text',
          'size' => 'tiny',
          'not null' => FALSE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    $value = $this->get('value')->getValue();
    return $value === NULL || $value === '';
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Hex value'));

    return $properties;
  }

}

Forty-something lines, three methods. That's a complete, working field type.

How it works

The @FieldType annotation — how Drupal finds this class at all

/**
 * @FieldType(
 *   id = "field_example_rgb",
 *   label = @Translation("Example Color RGB"),
 *   module = "field_example",
 *   description = @Translation("Demonstrates a field composed of an RGB color."),
 *   default_widget = "field_example_text",
 *   default_formatter = "field_example_simple_text"
 * )
 */

This isn't a regular comment — it's a machine-readable annotation, and it's the entire reason Drupal knows this plugin exists. Drupal scans every Plugin/Field/FieldType/ namespace across all enabled modules at cache-rebuild time, reads these annotations, and builds a registry of available field types from them. No separate registration file, no hook to implement — the annotation is the registration.

  • id = "field_example_rgb" — the machine name. This exact string gets written into your site's configuration and database the moment someone adds this field type to a content type.
  • label — the human-readable name a site builder sees in the "Choose a type of field" screen (you can see it in the screenshot below: "Example Color RGB").
  • module = "field_example" — which module owns this plugin, used for dependency tracking.
  • description — the one-line explainer shown under the label in that same screen.
  • default_widget and default_formatter — plugin IDs of the widget and formatter to use automatically if the site builder doesn't pick different ones. More on why these matter at the end of this lesson.
Beginner gotcha: if you write a field type class and it doesn't show up in the "Add field" list, the very first thing to check is whether you've cleared the cache. Drupal's plugin discovery is cached for performance — a brand-new annotation isn't picked up until the next cache rebuild.

Extending FieldItemBase

Notice the class doesn't implement a huge list of methods from scratch. FieldItemBase is the standard abstract base class almost every field type extends, and it already implements the bulk of Drupal's FieldItemInterface contract with sensible defaults. Because of that, RgbItem only needs to override the three methods that are specific to its own data: how it's stored, whether it counts as empty, and what its data properties look like. Also notice the class name itself: the Item suffix is a Drupal convention signaling "this represents one value" — a field can hold multiple values (an unlimited-cardinality field), and this class describes just one of them.

schema() — telling Drupal how to build the database column

public static function schema(FieldStorageDefinitionInterface $field_definition) {
  return [
    'columns' => [
      'value' => [
        'type' => 'text',
        'size' => 'tiny',
        'not null' => FALSE,
      ],
    ],
  ];
}

This is pure database plumbing. When a site builder adds this field to a content type, Drupal actually runs CREATE TABLE/ALTER TABLE statements behind the scenes, and this method is what tells it what columns to create. Here, one column named value, typed as a tiny text column (good for something short like a hex code such as ff5733), nullable so the field can be left empty. If your field type needed to store more than one piece of data per value — say, separate numeric fields for red, green, and blue instead of one combined string — you'd list multiple entries under columns here.

isEmpty() — deciding what counts as "nothing entered"

public function isEmpty() {
  $value = $this->get('value')->getValue();
  return $value === NULL || $value === '';
}

Drupal calls this every time an entity is saved, to decide whether a given field value is worth keeping or should be silently discarded. Get this wrong — for example, forget to check for an empty string and only check for NULL — and you can end up with junk rows in the database for fields the user never actually filled in.

propertyDefinitions() — the part that's easy to confuse with schema()

public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
  $properties['value'] = DataDefinition::create('string')
    ->setLabel(t('Hex value'));

  return $properties;
}

This is the one beginners most often mix up with schema(), so it's worth being explicit: schema() describes the physical database layout — actual columns, actual SQL types. propertyDefinitions() describes the logical data structure using Drupal's Typed Data API — the abstraction layer that lets totally different subsystems (the Validation API, REST, JSON:API, Views) all understand "this field has a property called value, and it behaves like a string" without needing to know anything about the underlying database. The property key 'value' here must match the column name used in schema() — that's how Drupal connects the logical property back to its physical storage.

Quick check: if you wanted this field to store three separate numeric values instead of one combined string, would that change belong in schema(), propertyDefinitions(), or both? (Answer: both — you'd need three columns in schema() and three matching entries in propertyDefinitions(), since each describes a different aspect of the same underlying data.)

See it for yourself

Visit /admin/structure/types/manage/article/fields/add-field on your DDEV site and start adding a new field. Scroll (or search) until you find "Example Color RGB" in the list of available field types, and click it.

The Add field dialog showing the custom Example Color RGB field type selected

That's the exact label and description from the annotation you just read, rendering as a real, selectable option in Drupal's field UI — proof that a 40-line PHP class with one annotation is genuinely all it takes to add a new kind of field to Drupal.

Key takeaways

  • The @FieldType annotation is the sole mechanism for registering a field type plugin — Drupal discovers it by scanning Plugin/Field/FieldType/ directories at cache-rebuild time. Forgot to clear the cache after adding one? That's why it's not showing up.
  • Extending FieldItemBase gives you sensible defaults and leaves you to implement just three methods for a functional field type: schema(), isEmpty(), and propertyDefinitions().
  • schema() maps directly to real database columns — its types and sizes affect both storage efficiency and the maximum length of values a user can enter.
  • isEmpty() runs at save time to discard blank field items; always check the actual property value, not just whether the object exists.
  • propertyDefinitions() bridges your field type to the Typed Data API, which is what makes it interoperable with REST, JSON:API, Views, and the validation system — describing what the data is, separately from how it's stored.
  • A field type on its own only handles storage. It's always paired with a widget (for editing) and a formatter (for display) — which is exactly what the next two lessons cover.

Coming up next

You've now defined what a field is — but nobody can actually type a value into it yet. Next up: the widget that puts an editable form element on the content edit form, and turns this field type from a database column into something an editor can actually use.