Skip to main content

Choosing the Right Field Types

The right field type is a schema decision that directly controls validation quality, editor speed, and template reliability.

Learning Focus

By the end of this lesson, you will map real content requirements to the most appropriate ACF/ACF Pro field types, avoid common overuse patterns (like text-for-everything), and enforce your decisions with code and CLI checks.

Concept Overview

Every field type represents a contract about value shape. A URL field promises a valid URL. A Number field promises numeric semantics. A Select field promises constrained vocabulary. If you choose a weaker type than the requirement demands, that contract disappears and template complexity grows.

Type selection should begin from output behavior, not input preference. Ask what rendering and querying need downstream: sorting, comparison, filtering, fallback, formatting, or layout composition. Then pick the narrowest type that satisfies those needs without blocking valid editorial workflows.

ACF Pro expands your modeling options significantly with Repeater, Flexible Content, Clone, Gallery, and advanced media structures. These are powerful, but only when chosen for actual data shape needs. Overusing complex types creates maintenance overhead similar to under-modeling with generic text fields.

Core Idea

Choose field types by data contract: use the most constrained type that still matches real editorial behavior.

Mental Model

Think of it as...Because...
Field type as a database column typeIt defines what values should be valid and how code should treat them
Select/Radio as enum constraintsThey prevent uncontrolled vocabulary drift
Repeater/Flexible as typed collectionsThey model repeated and composable content structures
URL/Email/Number as validation affordancesThey reduce invalid data before it reaches templates

Why It Matters

ApproachWhat HappensImpact in Production
Use specialized types (url, number, email) for typed dataInvalid values are blocked at input timeFewer template bugs and cleaner analytics
Use text for links, dates, and numeric valuesEvery template must re-validate and reformat dataSlower development and more edge-case failures
Use Select for controlled business statesEditors use approved values onlyReliable filtering/reporting and fewer content anomalies
Use Repeater for true repeated structuresRow-level rendering becomes predictableReusable components and easier API serialization
Use Flexible Content for everything (wrong pattern)Templates become overly dynamic and hard to testHigh maintenance cost and fragile release behavior

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
Text field'type' => 'text'Store short plain stringsGood for labels/headlines; weak for typed data
URL field'type' => 'url'Store and validate URL valuesPrefer over text for links and CTA targets
Number field'type' => 'number', 'min' => 0, 'max' => 9999Numeric input with boundsSafer for comparisons and calculations
Select field'type' => 'select', 'choices' => [...]Controlled vocabularyUse for statuses/tiers/categories not taxonomies
True/False field'type' => 'true_false'Binary feature flagsCleaner than text flags like yes/no
Date Picker'type' => 'date_picker', 'return_format' => 'Y-m-d'Date contracts for sorting/filteringKeep return format consistent across templates
Repeater field'type' => 'repeater', 'sub_fields' => [...]Ordered collection of repeated rowsACF Pro Required
Flexible Content'type' => 'flexible_content', 'layouts' => [...]Variable layout compositionACF Pro Required
acf_get_fields()`acf_get_fields(intstringarray $parent): array`

Practical Use Cases

Use Case 1 — Harden event metadata with typed fields

A webinar team keeps publishing invalid registration links and inconsistent event-date formats. You replace generic text inputs with typed fields that enforce data quality and reduce template fallback complexity.

  1. Register an event field group with Date, Select, URL, and True/False fields.
  2. Set required flags for registration-critical fields.
  3. Constrain event type with Select choices.
  4. Add min/max constraints where appropriate.
  5. Validate field types and test values via WP-CLI.
wp-content/themes/clinic-pro/inc/acf/event-field-types.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_event_metadata',
'title' => 'Event Metadata',
'fields' => [
[
'key' => 'field_event_date',
'label' => 'Event Date',
'name' => 'event_date',
'type' => 'date_picker',
'display_format' => 'Y-m-d',
'return_format' => 'Y-m-d',
'required' => 1,
],
[
'key' => 'field_event_type',
'label' => 'Event Type',
'name' => 'event_type',
'type' => 'select',
'required' => 1,
'choices' => [
'webinar' => 'Webinar',
'workshop' => 'Workshop',
'clinic' => 'Clinic Session',
],
],
[
'key' => 'field_event_registration_url',
'label' => 'Registration URL',
'name' => 'event_registration_url',
'type' => 'url',
'required' => 1,
],
[
'key' => 'field_event_is_limited',
'label' => 'Limited Seats',
'name' => 'event_is_limited',
'type' => 'true_false',
'ui' => 1,
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'event',
]]],
]);
});
terminal: command
wp post create --post_title="ACF Event Type Drill" --post_status=publish --post_type=event
wp eval '$id=(int)get_page_by_title("ACF Event Type Drill", OBJECT, "event")->ID; update_field("event_date","2026-03-15",$id); update_field("event_type","webinar",$id); update_field("event_registration_url","https://example.com/register",$id); update_field("event_is_limited",1,$id); echo get_field("event_type",$id) . " | " . get_field("event_registration_url",$id) . PHP_EOL;'
wp eval '$g=acf_get_field_groups(["key"=>"group_event_metadata"]); $f=acf_get_fields($g[0]); foreach($f as $field){echo $field["name"] . " => " . $field["type"] . PHP_EOL;}'
terminal: output
Success: Created post 1340.
webinar | https://example.com/register
event_date => date_picker
event_type => select
event_registration_url => url
event_is_limited => true_false
note

This is a textbook typed-model scenario: date, enum-like value, and URL each need their own validation contract.

Use Case 2 — Model product specifications with numeric and collection types

An ecommerce team currently stores dimensions and feature lists in plain text, making sorting and filtering unreliable. You switch to Number fields for dimensions and Repeater for structured feature rows.

  1. Register a product spec group with numeric dimensions.
  2. Add Select for stock status and True/False for warranty presence.
  3. Use Repeater for feature rows (name, value).
  4. Keep constraints explicit (min, max).
  5. Verify row data and field type map in CLI.
wp-content/themes/clinic-pro/inc/acf/product-field-types.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_product_spec_model',
'title' => 'Product Spec Model',
'fields' => [
[
'key' => 'field_product_width_mm',
'label' => 'Width (mm)',
'name' => 'product_width_mm',
'type' => 'number',
'min' => 1,
'max' => 5000,
'required' => 1,
],
[
'key' => 'field_product_height_mm',
'label' => 'Height (mm)',
'name' => 'product_height_mm',
'type' => 'number',
'min' => 1,
'max' => 5000,
'required' => 1,
],
[
'key' => 'field_product_stock_status',
'label' => 'Stock Status',
'name' => 'product_stock_status',
'type' => 'select',
'choices' => [
'in_stock' => 'In Stock',
'backorder' => 'Backorder',
'discontinued' => 'Discontinued',
],
'required' => 1,
],
[
'key' => 'field_product_has_warranty',
'label' => 'Has Warranty',
'name' => 'product_has_warranty',
'type' => 'true_false',
],
[
'key' => 'field_product_features',
'label' => 'Product Features',
'name' => 'product_features',
'type' => 'repeater',
'sub_fields' => [
[
'key' => 'field_product_feature_name',
'label' => 'Feature Name',
'name' => 'product_feature_name',
'type' => 'text',
],
[
'key' => 'field_product_feature_value',
'label' => 'Feature Value',
'name' => 'product_feature_value',
'type' => 'text',
],
],
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'product',
]]],
]);
});
terminal: command
wp post create --post_title="ACF Product Type Drill" --post_status=publish --post_type=product
wp eval '$id=(int)get_page_by_title("ACF Product Type Drill", OBJECT, "product")->ID; update_field("product_width_mm", 450, $id); update_field("product_height_mm", 920, $id); update_field("product_stock_status", "in_stock", $id); update_field("product_has_warranty", 1, $id); update_field("product_features", [["product_feature_name"=>"Power","product_feature_value"=>"220V"],["product_feature_name"=>"Weight","product_feature_value"=>"18kg"]], $id); echo get_field("product_stock_status", $id) . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("ACF Product Type Drill", OBJECT, "product")->ID; $rows=get_field("product_features",$id); echo count($rows) . PHP_EOL; echo $rows[0]["product_feature_name"] . PHP_EOL;'
terminal: output
Success: Created post 1341.
in_stock
2
Power
ACF Pro Required

This use case uses a Repeater field (product_features), which requires ACF Pro.

Use Case 3 — Edge case: text-only modeling forces fragile template parsing

A legacy project stores dimensions as text like "450x920" and stock states as arbitrary text ("available soon", "instock", "yes"). Templates now include brittle parsing and conditional branches that fail under inconsistent editor inputs.

❌ Fragile Pattern

wp-content/themes/clinic-pro/inc/acf/text-everywhere-fragile.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_fragile_product_fields',
'title' => 'Fragile Product Fields',
'fields' => [
[
'key' => 'field_product_dimensions_text',
'label' => 'Dimensions',
'name' => 'product_dimensions_text',
'type' => 'text',
],
[
'key' => 'field_product_stock_text',
'label' => 'Stock',
'name' => 'product_stock_text',
'type' => 'text',
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'product',
]]],
]);
});

✅ Robust Pattern

wp-content/themes/clinic-pro/inc/acf/typed-product-robust.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_typed_product_fields',
'title' => 'Typed Product Fields',
'fields' => [
[
'key' => 'field_typed_product_width_mm',
'label' => 'Width (mm)',
'name' => 'product_width_mm',
'type' => 'number',
'min' => 1,
'required' => 1,
],
[
'key' => 'field_typed_product_height_mm',
'label' => 'Height (mm)',
'name' => 'product_height_mm',
'type' => 'number',
'min' => 1,
'required' => 1,
],
[
'key' => 'field_typed_product_stock_status',
'label' => 'Stock Status',
'name' => 'product_stock_status',
'type' => 'select',
'choices' => [
'in_stock' => 'In Stock',
'backorder' => 'Backorder',
'discontinued' => 'Discontinued',
],
'required' => 1,
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'product',
]]],
]);
});

add_action('wp', function (): void {
if (!is_singular('product')) {
return;
}

$stock = (string) get_field('product_stock_status');
if (!in_array($stock, ['in_stock', 'backorder', 'discontinued'], true)) {
error_log('[typed-product] invalid stock status on post=' . get_the_ID());
}
});
terminal: command
wp eval '$id=(int)get_page_by_title("ACF Product Type Drill", OBJECT, "product")->ID; update_field("product_stock_text","available soon",$id); echo get_field("product_stock_text",$id) . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("ACF Product Type Drill", OBJECT, "product")->ID; update_field("product_stock_status","in_stock",$id); echo get_field("product_stock_status",$id) . PHP_EOL;'
terminal: output
available soon
in_stock
warning

Whenever templates must parse freeform text to derive structured meaning, your field type is probably wrong.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Using text fields for links and emailsConvenience over schema constraintsInvalid URLs/emails rendered publiclyUse url / email types and verify values with get_field()
Modeling enum states as free textNo controlled choices configuredFilters and analytics fail due value driftUse select with canonical choices
Using Flexible Content for simple fixed listsOver-modelingTemplate complexity and slower editorial workflowsUse Repeater or Group when layout is stable
Skipping min/max constraints on numbersValidation rules undefinedOut-of-range values break calculationsAdd min/max; audit with wp eval range checks
Mixing global settings into per-post fieldsContext confusionMassive repetitive edits and inconsistent copyMove global values to options and read with get_field(..., "option")
Not auditing field types after project growthIncremental drift over timeLegacy fields block refactors and testingRun periodic acf_get_fields() type inventory via CLI
Deep Dive: Why "Text for Everything" Becomes a Long-Term Liability

Text fields feel fast at creation time because they avoid up-front modeling decisions. But that speed is deceptive: every downstream consumer must implement validation, parsing, and fallback logic repeatedly. As projects grow, those ad-hoc checks diverge between templates, APIs, and migrations. You end up maintaining many small validators instead of one authoritative schema. A simple type inventory command often reveals how much hidden debt has accumulated.

wp eval 'foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){echo $group["title"] . " :: " . $field["name"] . " => " . $field["type"] . PHP_EOL;}}'

Best Practices

  1. Choose type from output contract: if code compares/sorts/calculates, avoid plain text.
  2. Constrain vocabularies with Select/Radio: never rely on editorial memory for status values.
  3. Use typed booleans for feature flags: true_false beats yes/no strings.
  4. Apply numeric constraints for measurable values: define min, max, and step semantics.
  5. Reserve Flexible Content for truly variable layouts: do not use it as default for all content.
  6. Audit field type inventory quarterly with wp eval: identify weak types before major refactors.
  7. Document type intent in model docs and code review checklist: schema decisions should be explicit.

Hands-On Practice

Exercise 1: Create a typed event model

Create wp-content/themes/clinic-pro/inc/acf/event-field-types.php from Use Case 1 and run:

wp eval '$g=acf_get_field_groups(["key"=>"group_event_metadata"]); echo count($g) . PHP_EOL;'

After completing this exercise, output should be:

1

Exercise 2: Seed event values and verify type-safe retrieval

Run:

wp post create --post_title="Typed Event Drill" --post_status=publish --post_type=event
wp eval '$id=(int)get_page_by_title("Typed Event Drill", OBJECT, "event")->ID; update_field("event_date","2026-04-10",$id); update_field("event_type","workshop",$id); update_field("event_registration_url","https://example.com/workshop",$id); echo get_field("event_type",$id) . PHP_EOL;'

After completing this exercise, expected final output:

workshop

Exercise 3: Create typed product dimensions and stock state

Run:

wp post create --post_title="Typed Product Drill" --post_status=publish --post_type=product
wp eval '$id=(int)get_page_by_title("Typed Product Drill", OBJECT, "product")->ID; update_field("product_width_mm", 320, $id); update_field("product_height_mm", 780, $id); update_field("product_stock_status","backorder",$id); echo get_field("product_stock_status",$id) . PHP_EOL;'

After completing this exercise, output should be:

backorder

Exercise 4: Inspect model type inventory

Run:

wp eval 'foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){echo $field["name"] . " => " . $field["type"] . PHP_EOL;}}'

After completing this exercise, output should include lines like:

event_registration_url => url
product_width_mm => number
product_features => repeater

Exercise 5: Detect text-overuse candidates

Run:

wp eval '$sus=[]; foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){if($field["type"]==="text" && preg_match("/(url|date|price|amount|status)/", $field["name"])){$sus[]=$field["name"];}}} print_r($sus);'

After completing this exercise, expected output should list candidate fields to refactor (or be empty if already clean):

Array
(
)

CLI Reference

CommandPurposeReal Example Output
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;'Count active model groups15
wp eval '$g=acf_get_field_groups(["key"=>"group_event_metadata"]); print_r(array_column(acf_get_fields($g[0]),"type"));'Inspect event field typesdate_picker select url true_false
wp eval '$id=1340; echo get_field("event_type",$id) . PHP_EOL;'Verify enum-like value retrievalwebinar
wp eval '$id=1341; echo get_field("product_width_mm",$id) . "x" . get_field("product_height_mm",$id) . PHP_EOL;'Validate numeric modeling450x920
wp post meta get 1341 product_stock_statusInspect raw stored stock statusin_stock
wp eval '$id=1341; $rows=get_field("product_features",$id); echo count($rows) . PHP_EOL;'Verify Repeater row count2
wp eval 'foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){echo $field["name"] . " => " . $field["type"] . PHP_EOL;}}'Full type inventory auditevent_date => date_picker
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro dependency

What's Next

tip

Revisit this lesson when a template starts accumulating input-validation code; that is usually a signal that the underlying field types are too weak.