Skip to content

SQLModel Integration

SQLModel combines SQLAlchemy tables with Pydantic validation in a single model class. Because SQLModel models are SQLAlchemy models under the hood, the starlette_admin.contrib.sqlmodel module serves as a thin wrapper around the existing SQLAlchemy backend.

Rather than implementing a separate system, this integration inherits all field auto-detection, primary key management, relationship handling, filtering, and session middleware directly from the core SQLAlchemy backend. It introduces a robust validation layer that runs submitted form data through your model's native Pydantic validators (such as Field(min_length=...) or custom @field_validator methods) before any database writes occur. Any resulting ValidationError exceptions are automatically translated into per-field form errors in the UI.

Note

Everything documented on the SQLAlchemy page applies unchanged. This includes synchronous and asynchronous engines, sessionmaker providers, the one-commit-per-request session lifecycle, relationship fields, and the filter registry.

Installation

pip install starlette-admin sqlmodel
uv add starlette-admin sqlmodel

Minimal Example

from sqlalchemy import create_engine
from sqlmodel import Field, SQLModel
from starlette_admin.contrib.sqlmodel import Admin, ModelView

engine = create_engine(
    "sqlite:///store.sqlite", connect_args={"check_same_thread": False}
)


class Product(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    name: str = Field(min_length=2)
    price: float


class ProductView(ModelView):
    fields = ["id", "name", "price"]


SQLModel.metadata.create_all(engine)
admin = Admin(engine, title="Store Admin", secret_key="change-me")
admin.add_view(ProductView(Product, icon="fa fa-box"))

The ModelView accepts the SQLModel table class directly and automatically derives the field list, forms, and filters from the model's schema.

Core Classes

sqlmodel.Admin

The sqlmodel.Admin class is the sqla.Admin class re-exported. It uses the same constructor, accepting an Engine, AsyncEngine, sessionmaker, or async_sessionmaker as its required session_provider argument. It also inserts the same session middleware that populates request.state.session on every request.

sqlmodel.ModelView

The sqlmodel.ModelView class inherits everything from sqla.ModelView and adds a validation layer. Its validate() method calls self.model.model_validate(data) before writing the record, ensuring form submissions are checked by the model's Pydantic validators instead of relying strictly on SQLAlchemy column constraints. File fields and relationship fields are intentionally excluded from this validation call because they fall outside the model's Pydantic validation surface.

from starlette_admin.contrib.sqlmodel import ModelView


class ArticleView(ModelView):
    fields = ["id", "title", "content", "author"]
    searchable_fields = ["title", "content"]

sqlmodel.InlineModelView

Inline views allow users to edit related rows inside the parent form. The class inherits foreign key detection and session handling from the SQLAlchemy InlineModelView and applies the same Pydantic validation to each inline row.

from starlette_admin.contrib.sqlmodel import InlineModelView, ModelView


class CommentInline(InlineModelView):
    model = Comment
    fields = ["id", "author_name", "body"]
    extra = 1


class ArticleView(ModelView):
    inlines = [CommentInline]

Pydantic Validation

Constraints declared on the model apply automatically to the create and edit forms:

from datetime import datetime

from pydantic import EmailStr
from sqlmodel import Field, Relationship, SQLModel


class Author(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    full_name: str = Field(min_length=2, index=True)
    email: EmailStr
    created_at: datetime | None = Field(default=None)

    articles: list["Article"] = Relationship(back_populates="author")

An input like a full_name shorter than two characters or an invalid email address will fail validation. These failures return as per-field form errors before any INSERT or UPDATE operation reaches the database.

Note

The EmailStr type requires the email-validator package, which is installable via pip install "pydantic[email]".

Full Working Example

This section provides a complete and runnable SQLModel integration with starlette-admin.

1. Install Dependencies

pip install starlette-admin sqlmodel "fastapi[standard]"
uv add starlette-admin sqlmodel "fastapi[standard]"

The fastapi[standard] package includes the FastAPI CLI, allowing you to start the development server by running fastapi dev.

2. Create the Application

Save the following code in a file named main.py.

main.py
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from enum import Enum

from fastapi import FastAPI
from sqlalchemy import Column, Text, create_engine
from sqlmodel import Field, Relationship, SQLModel
from starlette_admin import SlugField
from starlette_admin.contrib.sqlmodel import Admin, ModelView

engine = create_engine("sqlite:///blog.db")


class PostStatus(str, Enum):
    DRAFT = "DRAFT"
    PUBLISHED = "PUBLISHED"
    ARCHIVED = "ARCHIVED"


class Author(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    name: str = Field(min_length=2)

    posts: list["Post"] = Relationship(back_populates="author")


class Post(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    title: str = Field(min_length=3)
    slug: str = Field(unique=True)
    content: str = Field(sa_column=Column(Text))
    status: PostStatus = Field(default=PostStatus.DRAFT)
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    author_id: int | None = Field(foreign_key="author.id", default=None)
    author: Author | None = Relationship(back_populates="posts")


class AuthorView(ModelView):
    fields = ["id", "name", "posts"]


class PostView(ModelView):
    fields = [
        "id",
        "title",
        SlugField("slug", populate_from="title"),
        "content",
        "status",
        "created_at",
        "author",
    ]
    exclude_fields_from_create = ["created_at"]
    exclude_fields_from_edit = ["created_at"]
    searchable_fields = ["title", "content", "status"]
    fields_default_sort = [("created_at", True)]


@asynccontextmanager
async def lifespan(app: FastAPI):
    SQLModel.metadata.create_all(engine)
    yield


app = FastAPI(lifespan=lifespan)

admin = Admin(engine, title="Blog Admin", secret_key="change-me")
admin.add_view(AuthorView(Author, icon="fa fa-user"))
admin.add_view(PostView(Post, icon="fa fa-newspaper"))
admin.mount_to(app)

Submitting a title shorter than three characters or a name shorter than two characters re-renders the form with the error attached to the offending field.

3. Run the Server

Start the FastAPI development server:

fastapi dev
uv run -- fastapi dev

Navigate to http://127.0.0.1:8000/admin in your browser to view and interact with the admin dashboard.

Advanced Example: examples/14-sqlmodel in the repository contains a fully featured CMS example that includes relationships, inline views, actions, filters, events, and exports.

  • SQLAlchemy: The backend this integration builds on, covering engines, sessions, transactions, and the filter registry.
  • Views: Explore BaseModelView configuration options independent of the backend.
  • Filters: Learn about the filter builder and how ORM-specific filters plug in.