Fields
Fields are the building blocks of your views. Under the hood they're plain Python dataclasses: every attribute you pass to a field constructor becomes a dataclass field, and every field type subclasses BaseField, so you can inspect it, subclass it, or instantiate it directly.
Common attributes
Every field type inherits this set of configuration attributes from BaseField.
| Attribute | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | The attribute name on your model. |
label |
str | None |
Title-cased name |
The column header and form label. |
help_text |
str | None |
None |
Hint text shown below the form input. |
required |
bool |
False |
Requires a value in forms, on both the client and the server. |
validators |
list[Validator] |
[] |
Server-side validators run against the submitted value. See Validation. |
disabled |
bool |
False |
Greys out and locks the input in forms. |
read_only |
bool |
False |
Shows the field but blocks edits. |
default |
Any | Callable |
None |
The prefill value on the create form. |
getter |
Callable | None |
None |
Replaces the model attribute lookup when reading the value. See Computing, formatting, and parsing values. |
formatter |
dict[RequestAction, Callable] | None |
None |
Per-action display formatting, which replaces serialization for that action. See Computing, formatting, and parsing values. |
parser |
dict[RequestAction, Callable] | None |
None |
Per-action input parsing, which replaces the field's default parsing. See Computing, formatting, and parsing values. |
searchable |
bool |
True |
Included when the q search parameter matches. |
orderable |
bool |
True |
Adds a sort link in the list header. |
copy_to_clipboard |
bool |
False |
Adds a copy button next to the value on the detail page. |
filters |
list | None |
None |
Explicit override for the list page filters. |
extra |
dict[str, Any] |
{} |
A dictionary for your own metadata. |
Visibility controls
Use these boolean flags, all False by default, to control where a field appears:
exclude_from_listexclude_from_detailexclude_from_createexclude_from_editexclude_from_exportexclude_from_import
Defining defaults
The default attribute accepts a static value, a zero-argument callable, or a request-aware function:
from datetime import datetime
from starlette_admin import DateTimeField, StringField
StringField("status", default="draft") # Static value
DateTimeField("created_at", default=datetime.utcnow) # Zero-arg callable
StringField(
"locale", default=lambda request: request.state.admin_user.locale
) # Request-aware
Computing, formatting, and parsing values
Every field accepts three callable hooks, getter, formatter, and parser, that intercept and transform data as it moves between your model and the UI. Each accepts a synchronous or an asynchronous function.
getter: reading custom values
The getter hook replaces the default getattr() lookup when the field reads a model instance. The field calls getter(request, obj) and displays the return value.
from starlette_admin import StringField
# Displays a related author's email instead of a direct column value
StringField("author_email", getter=lambda request, obj: obj.author.email)
Because getter values rarely map to a physical database column, they pair best with a read-only display. ComputedField is a built-in shortcut for that combination.
formatter: transforming display output
The formatter hook sets how a stored value renders on specific pages. It maps a RequestAction, such as LIST, DETAIL, or EXPORT, to a (request, value) -> value callable.
from starlette_admin import RequestAction, StringField
StringField(
"api_key",
formatter={
# Mask the key on list views; show the full key on detail/export views
RequestAction.LIST: lambda request, value: (
f"{value[:4]}..." if value else "unset"
),
},
)
Formatting behavior to keep in mind:
- Nulls reach the formatter: Unlike default serialization, formatters receive
Nonevalues, so you can supply fallback text, such as"unset"above. - Serialization is bypassed: A matched formatter replaces the field's
serialize_valueandserialize_none_valuemethods. The return value is used as is, so the formatter is fully responsible for the final output. - JSON requirement: Values returned for the
LISTandRELATION_LOOKUPactions must stay JSON serializable.
parser: processing incoming data
The parser hook overrides the field's default parsing of submitted or imported data. It maps a RequestAction to a (request, raw) -> value callable.
- Forms (
CREATE,EDIT,INLINE_EDIT):rawis the submitted form input, or a list whenmultiple=True. - Imports (
IMPORT):rawis the unprocessed cell value from the file.
from starlette_admin import IntegerField, RequestAction
IntegerField(
"price",
parser={
# Strip currency symbols during import and convert to integer cents
RequestAction.IMPORT: lambda request, raw: int(
float(str(raw).strip("$")) * 100
),
},
)
After parsing, the returned value goes through the standard validation chain, required and then validators, exactly as if the field had parsed the data itself.
Hooks or a subclass?
For a one-off customization on a single field, you rarely need a subclass. Pass these hooks as constructor arguments to handle reading, display formatting, and input parsing. Subclass the field when you reuse the logic across views, or when you need to change the HTML rendering templates.
Validation
Server-side validation runs on every field when a create or edit form is submitted, so bad data never reaches the database.
The lifecycle is fixed:
- Empty values: When a submitted value is empty, such as
None,"", or an empty collection, only therequiredflag is checked. The validators are skipped. - Populated values: When data is present, each callable in the
validatorslist runs in order against the parsed value.
Validator signature
A validator receives four arguments: (request, field, value, form_values).
request: The current Starlette request object.field: The field instance being validated.value: The parsed value submitted for this field.form_values: A dictionary of all parsed form data, keyed by field name, so you can inspect other fields.
To reject a value, raise a ValueError. The admin catches the first error for a field, skips that field's remaining validators, and collects all errors to display next to their inputs.
Built-in validators
The starlette_admin.validators module provides standard rules:
from starlette_admin import IntegerField, StringField
from starlette_admin.validators import length, number_range
StringField("title", validators=[length(min=3, max=100)])
IntegerField("price", validators=[number_range(min=0)])
Custom and asynchronous validation
Write custom validators as synchronous or asynchronous functions. They receive the request, so they can query the database to check complex constraints.
async def unique_slug(request, field, value, form_values):
if await slug_exists(request.state.session, value):
raise ValueError("This slug is already taken")
StringField("slug", validators=[unique_slug])
With the form_values argument, a field-level validator can also enforce a rule that depends on another submitted field.
def not_before_start(request, field, value, form_values):
start = form_values.get("start_date")
if start is not None and value < start:
raise ValueError("End date cannot precede the start date")
DateField("end_date", validators=[not_before_start])
Context-specific validation rules
- Relation fields:
HasOneandHasManyreceive the primary keys of the related records during validation. - File fields: Validation runs once per
UploadFilein the payload. See File & media fields. - Cross-field validation: Use
form_valuesfor a simple dependency. For a rule that spans the whole form, override thevalidate()method on your view instead. View-level validation runs only after every field clears its own validation chain.
Storing custom metadata
extra is a plain dict that starlette-admin never reads or writes. Use it to attach your own data to a field instance, for a custom template, a hook in your BaseAdmin subclass, or any other integration point, without subclassing the field:
Text fields
StringField & TextAreaField
StringField renders a single-line text input for short content. TextAreaField extends it with a <textarea> element for long, multi-line text.
from starlette_admin import StringField, TextAreaField
from starlette_admin.contrib.sqla import ModelView
class PostView(ModelView):
fields = [
StringField("title", maxlength=200, placeholder="Post title"),
TextAreaField("content", rows=10),
]
| Extra attribute | Type | Default | Description |
|---|---|---|---|
maxlength and minlength |
int | None |
None |
HTML length constraints. |
placeholder |
str | None |
None |
Input placeholder text. |
rows (TextArea only) |
int |
6 |
Number of visible text lines. |
TinyMCEEditorField
Extends TextAreaField with a WYSIWYG editor from the TinyMCE library. It requires the tinymce extra package.
from starlette_admin import TinyMCEEditorField
TinyMCEEditorField("content", height=400, toolbar="undo redo | bold italic")
Note
The height, menubar, statusbar, and toolbar attributes control the editor's UI. Pass any other native TinyMCE configuration through extra_options.
Formatted text fields
These StringField variants render a matching HTML input type and format the value when the record is displayed.
EmailField(type="email")URLField(type="url")PhoneField(type="tel")ColorField(type="color")UUIDField(type="text")IPAddressField(type="text")
Note
EmailField, URLField, UUIDField, and IPAddressField each add a matching validator (email, url, uuid, and ip_address from starlette_admin.validators) when you leave validators empty. Pass your own validators to override it.
UUIDField sets copy_to_clipboard=True by default. IPAddressField accepts ipv4, True by default, and ipv6, False by default, which control the address families its default validator accepts.
PasswordField
Renders an <input type="password"> element on forms to obscure what the user types.
Danger
PasswordField masks the input on create and edit forms only. It doesn't override the display templates, so values render as plain text on list and detail pages, and it logs raw submitted values at DEBUG level.
Set exclude_from_list = True and exclude_from_detail = True on password fields, and turn off DEBUG logging in production.
Numeric fields
Numeric fields handle integers, floats, and decimals.
from starlette_admin import DecimalField, FloatField, IntegerField
from starlette_admin.contrib.sqla import ModelView
class ProductView(ModelView):
fields = [
IntegerField("stock", min=0, max=10_000),
FloatField("rating"),
DecimalField("price", min=0, step="0.01"),
]
| Extra attribute | Applies to | Description |
|---|---|---|
min and max |
Integer, Decimal | Minimum and maximum allowed values. |
step |
Integer, Decimal | The increment step constraint. |
Note
FloatField works differently: it renders as a plain text input, coerces the submission to a float, and doesn't support min, max, or step.
Date & time fields
These fields use the native browser date and time pickers, backed by the matching standard library types (datetime.date, datetime.datetime, and datetime.time).
from starlette_admin import DateField, DateTimeField, TimeField
from starlette_admin.contrib.sqla import ModelView
class EventView(ModelView):
fields = [
DateField("event_date"),
DateTimeField("starts_at", output_format="medium"),
TimeField("daily_reminder"),
]
| Extra attribute | Type | Default | Description |
|---|---|---|---|
output_format |
str | None |
None |
Babel display format: "short", "medium", "long", "full", or a custom pattern. |
search_format |
str | None |
ORM-specific | Format used to build database search queries. |
Note
When timezone support is on, DateTimeField converts between the display timezone and the database timezone for you.
ArrowField
A DateTimeField variant backed by an Arrow object. Outside edit forms, it displays a humanized relative time, such as "3 hours ago". It requires the arrow package.
Selection & collection fields
EnumField
The general-purpose select field. It renders a <select> dropdown, or a select2 multi-select when multiple=True. Back it with a Python Enum subclass, a list of tuples, or choices loaded at request time.
import enum
from starlette_admin import EnumField
from starlette_admin.contrib.sqla import ModelView
class Status(str, enum.Enum):
DRAFT = "draft"
PUBLISHED = "published"
class PostView(ModelView):
fields = [
EnumField("status", enum=Status),
EnumField("language", choices=[("en", "English"), ("fr", "French")]),
]
| Extra attribute | Type | Description |
|---|---|---|
enum |
type[Enum] | None |
Build choices from a Python Enum class. |
choices |
Sequence | None |
Static (value, label) pairs, or bare values. |
choices_loader |
Callable | None |
Compute choices per request. |
multiple |
bool |
Turns on multi-select and stores values as a list. |
Important
Provide exactly one of enum, choices, or choices_loader.
TimeZoneField, CountryField, and CurrencyField are EnumField subclasses backed by Babel locale data, which needs the i18n extra. They localize their labels to the current request.
TagsField
A free-text tagging input built on select2. It stores a list[str] and needs no predefined choices.
ListField
Wraps another field to store an ordered list of values of that type. It renders as repeatable rows with add and remove controls. The wrapped field's name becomes the ListField's name.
from starlette_admin import ListField, StringField
# Renders a repeatable list of string inputs
fields = [ListField(StringField("gallery_urls"))]
CollectionField
Groups several subfields into one nested object. Use it for embedded or struct-like data, such as a MongoDB embedded document.
from starlette_admin import CollectionField, IntegerField, StringField
fields = [
CollectionField(
"shipping_address",
fields=[
StringField("street"),
StringField("city"),
IntegerField("floor", required=False),
],
),
]
Specialized fields
JSONField
Renders a JSON tree and code editor, and stores a Python dict. Pass a standard JSON Schema dictionary to validation_schema for client-side feedback.
SlugField
A StringField variant that fills itself in on the client from another field's input. A manual edit stops the auto-fill.
from starlette_admin import SlugField, StringField
fields = [
StringField("title"),
SlugField("slug", populate_from="title"),
]
Important
populate_from is required and must point to another field on the same form. The generated slug is submitted and stored like any other string.
ComputedField
A read-only, virtual field derived from the model instance at display time, with no database column behind it. It builds on the getter hook that every field has, and adds the defaults a virtual column needs: excluded from create forms, read-only, non-searchable, and non-orderable.
from starlette_admin import ComputedField
fields = [
"first_name",
"last_name",
ComputedField(
"full_name", getter=lambda request, obj: f"{obj.first_name} {obj.last_name}"
),
]
For complex or reusable logic, subclass ComputedField and override parse_obj() instead of passing an inline getter:
class FullNameField(ComputedField):
async def parse_obj(self, request, obj) -> str:
return f"{obj.first_name} {obj.last_name}"
getter and parse_obj do the same job: use getter for short expressions, and subclass ComputedField when the logic spans several lines or is reused across views. On edit forms, the field still appears as plain-text display, so the user sees the current computed value.
Every ComputedField subclass keeps StringField rendering. To compute a value that should render as another type, such as a date, a badge, or an image, set getter= on that field type directly, along with the matching read_only and exclude_from_* flags.
File & media fields
FileField renders a file upload input, and ImageField adds an image preview and a validity check. Attach a storage= backend to save uploads automatically and store a JSON FileInfo dictionary in the database. For the full configuration, see the File Storage guide.
from starlette_admin import FileField, ImageField
from starlette_admin.contrib.sqla import ModelView
from starlette_admin.storage import LocalStorage
covers_storage = LocalStorage(base_dir="uploads/covers", name="covers")
documents_storage = LocalStorage(base_dir="uploads/documents", name="documents")
class ArticleView(ModelView):
fields = [
"id",
"title",
ImageField(
"cover",
storage=covers_storage,
upload_folder="covers",
max_size=5 * 1024 * 1024,
thumbnail_size=(50, 50),
),
FileField(
"document",
storage=documents_storage,
upload_folder="documents",
accept=".pdf,.doc,.docx",
),
]
| Extra attribute | Type | Default | Description |
|---|---|---|---|
accept |
str | None |
None |
Comma-separated list of accepted file extensions or MIME types, passed to the HTML accept attribute. |
multiple |
bool |
False |
Accepts several files in one field. |
storage |
BaseStorage | None |
None |
Storage backend that saves the uploads. Without it, the field hands raw uploads to your backend. |
upload_folder |
str |
"" |
The storage-relative folder for saved files. |
max_size |
int | None |
None |
Maximum accepted upload size, in bytes. |
validators |
list[Validator] |
[] |
Custom validators, each called as (request, field, upload) once per uploaded file, after the accept and max_size checks. Raise ValueError to reject. |
thumbnail_size |
tuple[int, int] | None |
None |
ImageField only. When set, Pillow generates a bounded thumbnail at save time, and the list page uses it in place of the full image. |
Note
ImageField prepends a Pillow-based image validity check to the validators list. When Pillow is installed and storage is configured, it also records width and height in the resulting FileInfo.
With thumbnail_size set, the admin generates a thumbnail alongside the full image, preserving the aspect ratio and never upscaling, and stores it under its own key. For example, covers/cat.jpg gets a covers/cat.thumb.jpg sibling. The list page uses the thumbnail automatically. Rows without one, from pre-existing data or because thumbnail_size is unset, fall back to the full image. A thumbnail generation failure is logged and never fails the upload.
The detail page opens every ImageField image in a lightbox, so viewers can page through full-resolution images. Images that belong to the same field (multiple=True) are grouped into one gallery.
See examples/04-filestorage for a complete runnable app, including a custom MIME-type validator.
Without a storage
With no storage= attached, the field hands uploads to your backend raw instead of saving them:
- In create and edit forms, the parsed value is a tuple,
(UploadFile | list[UploadFile] | None, bool). The first element is the raw StarletteUploadFile, a list whenmultiple=True, orNonewhen the user selected nothing. The second element isTruewhen the user selects the delete box on the edit form, which means they want the existing file removed without a replacement. Your backend'screate()andedit()logic stores the upload and honors the delete flag. - On list and detail pages, the field expects the value to expose three keys, as a
dict, or three attributes, as an object:url, required, the link target;filename, the display label; andcontent_type, which selects the file-type icon.
This contract is how the ORM integrations below plug their own file handling into the same field.
ORM-native file columns
MongoEngine supports mongoengine.FileField and mongoengine.ImageField out of the box, with GridFS as the storage. The admin uploads to, serves from, and deletes files in GridFS for you. You need no storage= configuration: list the field by name.
SQLAlchemy gets the same treatment through sqlalchemy-file. Declare its FileField or ImageField column types on your models, and starlette-admin detects them, renders the matching admin field, and registers a route to serve the stored files. You configure storage through sqlalchemy-file's own StorageManager, backed by Apache Libcloud containers, and uploads join the session transaction, so a rolled-back session discards the stored file.
import os
from libcloud.storage.drivers.local import LocalStorageDriver
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy_file import ImageField
from sqlalchemy_file.storage import StorageManager
from sqlalchemy_file.validators import SizeValidator
from starlette_admin.contrib.sqla import ModelView
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "author"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
avatar = mapped_column(
ImageField(
upload_storage="avatar",
thumbnail_size=(50, 50),
validators=[SizeValidator("200k")],
)
)
# sqlalchemy-file storage setup, independent of starlette-admin's BaseStorage
os.makedirs("upload/avatars", exist_ok=True)
StorageManager.add_storage(
"avatar", LocalStorageDriver("upload").get_container("avatars")
)
class AuthorView(ModelView):
fields = ["id", "name", "avatar"]
See examples/13-sqlachemy-file for a full app with several storages, content-type validation, and multiple=True fields.
HasOne & HasMany
Relational fields that render as select2 inputs, backed by the related view's search endpoint.
from starlette_admin import HasMany, HasOne, IntegerField, StringField
from starlette_admin.contrib.sqla import Admin, ModelView
class AuthorView(ModelView):
fields = [
IntegerField("id"),
StringField("name"),
HasMany("books", key="book"),
]
class BookView(ModelView):
fields = [
IntegerField("id"),
StringField("title"),
HasOne("author", key="author"),
]
The key parameter points to the matching ModelView. Register both views on the same Admin instance so the keys resolve.
What's next
- Filters: Customize the filter builder on your list pages.
- File Storage: Configure storage backends for
FileFieldandImageField. - Custom Fields: Build a custom field.