Skip to content

Custom Fields

Learn how to override automatic field detection and configure custom field behavior.


Overview

While Pydantic Airtable provides field type detection, you may need to:

  • Override detected types
  • Use custom field names in Airtable
  • Configure field options
  • Mark fields as read-only

The airtable_field Function

Use airtable_field() to customize field behavior:

from pydantic_airtable import airtable_field, AirtableFieldType

field_name: str = airtable_field(
    field_type=AirtableFieldType.LONG_TEXT,  # Override type
    field_name="Field Name in Airtable",      # Custom Airtable name
    read_only=False,                          # Whether field is read-only
    choices=["Option 1", "Option 2"],         # For SELECT/MULTI_SELECT
    default=None,                             # Default value (Pydantic)
    description="Field description"           # Pydantic field description
)

Override Field Type

Basic Override

from pydantic_airtable import airtable_model, airtable_field, AirtableFieldType
from pydantic import BaseModel

@airtable_model(table_name="Products")
class Product(BaseModel):
    name: str

    # 'notes' would be detected as LONG_TEXT, but we want single line
    notes: str = airtable_field(
        field_type=AirtableFieldType.SINGLE_LINE_TEXT
    )

    # 'data' is generic, explicitly set to LONG_TEXT
    data: str = airtable_field(
        field_type=AirtableFieldType.LONG_TEXT
    )

All Available Types

from pydantic_airtable import AirtableFieldType

# Text types
AirtableFieldType.SINGLE_LINE_TEXT  # Single line text
AirtableFieldType.LONG_TEXT         # Multi-line text
AirtableFieldType.EMAIL             # Email with validation
AirtableFieldType.URL               # URL with validation
AirtableFieldType.PHONE             # Phone number

# Number types
AirtableFieldType.NUMBER            # Generic number
AirtableFieldType.CURRENCY          # Currency with symbol
AirtableFieldType.PERCENT           # Percentage

# Date/Time types
AirtableFieldType.DATE              # Date only
AirtableFieldType.DATETIME          # Date and time

# Selection types
AirtableFieldType.SELECT            # Single select
AirtableFieldType.MULTI_SELECT      # Multiple select
AirtableFieldType.CHECKBOX          # Boolean checkbox

# Other types
AirtableFieldType.ATTACHMENT        # File attachments
AirtableFieldType.FORMULA           # Computed field
AirtableFieldType.ROLLUP            # Rollup from linked records
AirtableFieldType.LOOKUP            # Lookup from linked records

Custom Field Names

Map Python field names to different Airtable field names:

@airtable_model(table_name="Users")
class User(BaseModel):
    # Python: user_name → Airtable: "Full Name"
    user_name: str = airtable_field(
        field_name="Full Name"
    )

    # Python: contact_email → Airtable: "Email Address"
    contact_email: str = airtable_field(
        field_name="Email Address"
    )

    # Python: created → Airtable: "Created Date"
    created: datetime = airtable_field(
        field_name="Created Date"
    )

When to Use Custom Names

  • Airtable fields have spaces: "First Name", "Phone Number"
  • Airtable fields have special characters: "Cost ($)", "Completion %"
  • Legacy tables with non-Pythonic names
  • Integration with existing Airtable bases

Select Fields with Choices

Single Select

@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str

    # Define available choices
    status: str = airtable_field(
        field_type=AirtableFieldType.SELECT,
        choices=["To Do", "In Progress", "Review", "Done"],
        default="To Do"
    )

    priority: str = airtable_field(
        field_type=AirtableFieldType.SELECT,
        choices=["Low", "Medium", "High", "Urgent"],
        default="Medium"
    )

Multi Select

from typing import List

@airtable_model(table_name="Projects")
class Project(BaseModel):
    name: str

    # Multiple selections allowed
    tags: List[str] = airtable_field(
        field_type=AirtableFieldType.MULTI_SELECT,
        choices=["Frontend", "Backend", "Database", "DevOps", "Design"],
        default=[]
    )

    team_members: List[str] = airtable_field(
        field_type=AirtableFieldType.MULTI_SELECT,
        choices=["Alice", "Bob", "Carol", "Dave"],
        default=[]
    )

Using Enums (Alternative)

from enum import Enum

class Status(str, Enum):
    TODO = "To Do"
    IN_PROGRESS = "In Progress"
    DONE = "Done"

@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str
    status: Status = Status.TODO  # Automatically becomes SELECT

Read-Only Fields

Mark fields that shouldn't be sent during create/update:

@airtable_model(table_name="Records")
class Record(BaseModel):
    name: str

    # Auto-number field (Airtable generates this)
    record_number: int = airtable_field(
        read_only=True,
        default=0
    )

    # Formula field (computed by Airtable)
    calculated_value: str = airtable_field(
        field_type=AirtableFieldType.FORMULA,
        read_only=True,
        default=""
    )

    # Last modified (updated by Airtable)
    last_modified: datetime = airtable_field(
        read_only=True,
        default=None
    )

Common Read-Only Fields

  • Auto-number fields
  • Formula fields
  • Rollup fields
  • Lookup fields
  • Created time (use built-in created_time)
  • Last modified time
  • Created by / Modified by

Field Options

Currency Options

@airtable_model(table_name="Products")
class Product(BaseModel):
    name: str

    # USD with 2 decimal places
    price_usd: float = airtable_field(
        field_type=AirtableFieldType.CURRENCY,
        json_schema_extra={
            "precision": 2,
            "symbol": "$"
        }
    )

    # Euro with 2 decimal places
    price_eur: float = airtable_field(
        field_type=AirtableFieldType.CURRENCY,
        json_schema_extra={
            "precision": 2,
            "symbol": "€"
        }
    )

Percentage Options

@airtable_model(table_name="Metrics")
class Metrics(BaseModel):
    name: str

    # Percentage with 1 decimal place
    completion: float = airtable_field(
        field_type=AirtableFieldType.PERCENT,
        json_schema_extra={
            "precision": 1
        }
    )

Number Precision

@airtable_model(table_name="Measurements")
class Measurement(BaseModel):
    name: str

    # Integer (no decimals)
    count: int = airtable_field(
        field_type=AirtableFieldType.NUMBER,
        json_schema_extra={
            "precision": 0
        }
    )

    # 4 decimal places
    precise_value: float = airtable_field(
        field_type=AirtableFieldType.NUMBER,
        json_schema_extra={
            "precision": 4
        }
    )

Combining with Pydantic Validation

airtable_field() accepts all Pydantic Field() parameters:

from pydantic import field_validator

@airtable_model(table_name="Users")
class User(BaseModel):
    # With Pydantic constraints
    name: str = airtable_field(
        min_length=1,
        max_length=100,
        description="User's full name"
    )

    # With pattern validation
    email: str = airtable_field(
        pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$',
        description="Valid email address"
    )

    # With numeric constraints
    age: int = airtable_field(
        ge=0,
        le=150,
        default=None,
        description="Age in years"
    )

    # Custom validator still works
    @field_validator('name')
    @classmethod
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v.strip()

Complex Example

Here's a comprehensive example combining multiple customizations:

from pydantic_airtable import airtable_model, airtable_field, AirtableFieldType
from pydantic import BaseModel, field_validator
from typing import Optional, List
from datetime import datetime
from enum import Enum

class Priority(str, Enum):
    LOW = "Low"
    MEDIUM = "Medium"
    HIGH = "High"

@airtable_model(table_name="Tasks")
class Task(BaseModel):
    # Standard fields with type detection
    title: str
    description: Optional[str] = None  # → LONG_TEXT (detected)

    # Custom Airtable field name
    assignee_email: str = airtable_field(
        field_name="Assigned To",
        field_type=AirtableFieldType.EMAIL
    )

    # Select with explicit choices
    status: str = airtable_field(
        field_name="Task Status",
        field_type=AirtableFieldType.SELECT,
        choices=["Backlog", "To Do", "In Progress", "Review", "Done"],
        default="Backlog"
    )

    # Enum-based select (auto-detected)
    priority: Priority = Priority.MEDIUM

    # Multi-select tags
    tags: List[str] = airtable_field(
        field_type=AirtableFieldType.MULTI_SELECT,
        choices=["Bug", "Feature", "Documentation", "Refactor"],
        default=[]
    )

    # Currency field
    estimated_cost: Optional[float] = airtable_field(
        field_name="Estimated Cost ($)",
        field_type=AirtableFieldType.CURRENCY,
        json_schema_extra={"precision": 2, "symbol": "$"},
        default=None
    )

    # Read-only computed field
    task_number: int = airtable_field(
        field_name="Task #",
        read_only=True,
        default=0
    )

    # Pydantic validation
    @field_validator('title')
    @classmethod
    def title_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('Title cannot be empty')
        return v.strip()

Troubleshooting

Field Not Saving

# Problem: Field value not saving to Airtable
notes: str = airtable_field(read_only=True)  # read_only prevents saving

# Solution: Remove read_only or set to False
notes: str = airtable_field(read_only=False)

Wrong Type in Airtable

# Problem: Field created with wrong type
status: str  # Created as SINGLE_LINE_TEXT, wanted SELECT

# Solution: Explicitly specify type
status: str = airtable_field(
    field_type=AirtableFieldType.SELECT,
    choices=["Active", "Inactive"]
)

Field Name Mismatch

# Problem: Python field doesn't match Airtable field
first_name: str  # Airtable has "First Name"

# Solution: Use field_name parameter
first_name: str = airtable_field(field_name="First Name")

Next Steps