Choosing the Right Field Types
The right field type is a schema decision that directly controls validation quality, editor speed, and template reliability.
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.
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 type | It defines what values should be valid and how code should treat them |
| Select/Radio as enum constraints | They prevent uncontrolled vocabulary drift |
| Repeater/Flexible as typed collections | They model repeated and composable content structures |
| URL/Email/Number as validation affordances | They reduce invalid data before it reaches templates |
Why It Matters
| Approach | What Happens | Impact in Production |
|---|---|---|
Use specialized types (url, number, email) for typed data | Invalid values are blocked at input time | Fewer template bugs and cleaner analytics |
Use text for links, dates, and numeric values | Every template must re-validate and reformat data | Slower development and more edge-case failures |
| Use Select for controlled business states | Editors use approved values only | Reliable filtering/reporting and fewer content anomalies |
| Use Repeater for true repeated structures | Row-level rendering becomes predictable | Reusable components and easier API serialization |
| Use Flexible Content for everything (wrong pattern) | Templates become overly dynamic and hard to test | High maintenance cost and fragile release behavior |
Reference Table
| Term/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
| Text field | 'type' => 'text' | Store short plain strings | Good for labels/headlines; weak for typed data |
| URL field | 'type' => 'url' | Store and validate URL values | Prefer over text for links and CTA targets |
| Number field | 'type' => 'number', 'min' => 0, 'max' => 9999 | Numeric input with bounds | Safer for comparisons and calculations |
| Select field | 'type' => 'select', 'choices' => [...] | Controlled vocabulary | Use for statuses/tiers/categories not taxonomies |
| True/False field | 'type' => 'true_false' | Binary feature flags | Cleaner than text flags like yes/no |
| Date Picker | 'type' => 'date_picker', 'return_format' => 'Y-m-d' | Date contracts for sorting/filtering | Keep return format consistent across templates |
| Repeater field | 'type' => 'repeater', 'sub_fields' => [...] | Ordered collection of repeated rows | ACF Pro Required |
| Flexible Content | 'type' => 'flexible_content', 'layouts' => [...] | Variable layout composition | ACF Pro Required |
acf_get_fields() | `acf_get_fields(int | string | array $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.
- Register an event field group with Date, Select, URL, and True/False fields.
- Set required flags for registration-critical fields.
- Constrain event type with Select choices.
- Add min/max constraints where appropriate.
- Validate field types and test values via WP-CLI.
<?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',
]]],
]);
});
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;}'
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
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.
- Register a product spec group with numeric dimensions.
- Add Select for stock status and True/False for warranty presence.
- Use Repeater for feature rows (
name,value). - Keep constraints explicit (
min,max). - Verify row data and field type map in CLI.
<?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',
]]],
]);
});
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;'
Success: Created post 1341.
in_stock
2
Power
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
<?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
<?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());
}
});
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;'
available soon
in_stock
Whenever templates must parse freeform text to derive structured meaning, your field type is probably wrong.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
| Using text fields for links and emails | Convenience over schema constraints | Invalid URLs/emails rendered publicly | Use url / email types and verify values with get_field() |
| Modeling enum states as free text | No controlled choices configured | Filters and analytics fail due value drift | Use select with canonical choices |
| Using Flexible Content for simple fixed lists | Over-modeling | Template complexity and slower editorial workflows | Use Repeater or Group when layout is stable |
| Skipping min/max constraints on numbers | Validation rules undefined | Out-of-range values break calculations | Add min/max; audit with wp eval range checks |
| Mixing global settings into per-post fields | Context confusion | Massive repetitive edits and inconsistent copy | Move global values to options and read with get_field(..., "option") |
| Not auditing field types after project growth | Incremental drift over time | Legacy fields block refactors and testing | Run 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
- Choose type from output contract: if code compares/sorts/calculates, avoid plain text.
- Constrain vocabularies with Select/Radio: never rely on editorial memory for status values.
- Use typed booleans for feature flags:
true_falsebeatsyes/nostrings. - Apply numeric constraints for measurable values: define
min,max, and step semantics. - Reserve Flexible Content for truly variable layouts: do not use it as default for all content.
- Audit field type inventory quarterly with
wp eval: identify weak types before major refactors. - 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
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;' | Count active model groups | 15 |
wp eval '$g=acf_get_field_groups(["key"=>"group_event_metadata"]); print_r(array_column(acf_get_fields($g[0]),"type"));' | Inspect event field types | date_picker select url true_false |
wp eval '$id=1340; echo get_field("event_type",$id) . PHP_EOL;' | Verify enum-like value retrieval | webinar |
wp eval '$id=1341; echo get_field("product_width_mm",$id) . "x" . get_field("product_height_mm",$id) . PHP_EOL;' | Validate numeric modeling | 450x920 |
wp post meta get 1341 product_stock_status | Inspect raw stored stock status | in_stock |
wp eval '$id=1341; $rows=get_field("product_features",$id); echo count($rows) . PHP_EOL;' | Verify Repeater row count | 2 |
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 audit | event_date => date_picker |
| `wp plugin list --status=active | grep advanced-custom-fields-pro` | Confirm ACF Pro dependency |
What's Next
- Continue to Repeater, Group, Clone, and Flexible Content Patterns.
- Return to Module 2 Overview for the full modeling sequence.
- Related lesson: Field Groups, Location Rules, and Naming Conventions.
- Related lesson: Reading Field Values in Templates.
Revisit this lesson when a template starts accumulating input-validation code; that is usually a signal that the underlying field types are too weak.