Skip to content

Models

Define your data structure using Python classes that map directly to PostgreSQL tables.


What is a Model?

A model is a Python class that represents a table in your database. Each model:

  • Defines what data you want to store (fields like title, email, price)
  • Maps to a PostgreSQL table automatically
  • Provides methods to create, read, update, and delete records
  • Validates data before saving

Think of a model as a blueprint: it describes what a "Post" or "User" looks like, and Aksara handles all the database work.

from aksara import Model, fields

class Article(Model):
    """A blog article stored in the 'articles' table."""

    title = fields.String(max_length=200)
    content = fields.Text()
    published = fields.Boolean(default=False)
    created_at = fields.DateTime(auto_now_add=True)

What this creates:

PostgreSQL table: articles
┌──────────────┬─────────────────┬─────────────┐
│ Column       │ Type            │ Constraints │
├──────────────┼─────────────────┼─────────────┤
│ id           │ UUID            │ PRIMARY KEY │
│ title        │ VARCHAR(200)    │ NOT NULL    │
│ content      │ TEXT            │ NOT NULL    │
│ published    │ BOOLEAN         │ DEFAULT false│
│ created_at   │ TIMESTAMPTZ     │ AUTO        │
└──────────────┴─────────────────┴─────────────┘


Creating a Model

Step 1: Import the Base Class and Fields

from aksara import Model, fields
  • Model — The base class all your models inherit from
  • fields — Contains all field types (String, Integer, Boolean, etc.)

Step 2: Define Your Class

class Task(Model):
    """A task that users can complete."""

    title = fields.String(max_length=200)
    description = fields.Text(nullable=True)
    completed = fields.Boolean(default=False)
    due_date = fields.DateTime(nullable=True)

Step 3: Create the Table

Run migrations to create the actual PostgreSQL table:

aksara makemigrations
aksara migrate

What You Get Automatically

Primary Key (id)

Every model automatically gets a UUID primary key. You don't need to define it:

class User(Model):
    email = fields.Email(unique=True)
    # 'id' is automatically added as a UUID

user = await User.objects.create(email="alice@example.com")
print(user.id)  # UUID('550e8400-e29b-41d4-a716-446655440000')

Why UUID instead of auto-increment?

  • Globally unique (safe for distributed systems)
  • Can be generated client-side
  • No sequential pattern (better security)
  • PostgreSQL-native with gen_random_uuid()

Table Name

The table name is derived from your class name:

Model Name Table Name
Article articles
User users
Category categories
UserProfile user_profiles

Override the table name if needed:

class Article(Model):
    __tablename__ = "blog_posts"  # Custom table name

    title = fields.String(max_length=200)

Timestamps

Add automatic timestamps with auto_now and auto_now_add:

class Article(Model):
    title = fields.String(max_length=200)

    # Set once when created, never changes
    created_at = fields.DateTime(auto_now_add=True)

    # Updated every time you save
    updated_at = fields.DateTime(auto_now=True)

Working with Models

Creating Records

# Method 1: Create and save in one step
article = await Article.objects.create(
    title="Hello World",
    content="My first article!",
    published=True,
)

# Method 2: Create an instance, then save
article = Article(
    title="Hello World",
    content="My first article!",
)
await article.save()

Reading Records

# Get one record by ID
article = await Article.objects.get(id=article_id)

# Get one record by any field
article = await Article.objects.get(title="Hello World")

# Get all records
articles = await Article.objects.all()

# Filter records
published = await Article.objects.filter(published=True)

Updating Records

# Method 1: Modify and save
article = await Article.objects.get(id=article_id)
article.title = "New Title"
await article.save()

# Method 2: Update multiple at once
await Article.objects.filter(published=False).update(published=True)

Deleting Records

# Delete one record
article = await Article.objects.get(id=article_id)
await article.delete()

# Delete multiple records
await Article.objects.filter(published=False).delete()

Field Types

Here's a quick reference of common fields:

Text Fields

class Example(Model):
    # Short text with max length
    name = fields.String(max_length=100)

    # Unlimited text
    bio = fields.Text()

    # Email with validation
    email = fields.Email(unique=True)

    # URL with validation
    website = fields.URL(nullable=True)

Numeric Fields

class Product(Model):
    # Whole numbers
    quantity = fields.Integer(default=0)

    # Decimal numbers (for money)
    price = fields.Decimal(max_digits=10, decimal_places=2)

    # Floating point (for calculations)
    rating = fields.Float(nullable=True)

Date and Time

class Event(Model):
    # Date only (no time)
    date = fields.Date()

    # Time only (no date)
    start_time = fields.Time()

    # Full timestamp with timezone
    created_at = fields.DateTime(auto_now_add=True)

Other Types

class Settings(Model):
    # True/False
    is_active = fields.Boolean(default=True)

    # JSON data (dictionaries, lists)
    preferences = fields.JSON(default=dict)

    # PostgreSQL arrays
    tags = fields.Array(base_type="text", default=list)

    # UUID
    external_id = fields.UUID(nullable=True)

See Fields Reference for complete documentation.


Field Options

All fields accept these common options:

Option What It Does Example
nullable Allow NULL values fields.String(nullable=True)
default Default value fields.Boolean(default=False)
unique Ensure uniqueness fields.Email(unique=True)
db_index Create database index fields.String(db_index=True)
primary_key Mark as primary key fields.UUID(primary_key=True)

Examples

class User(Model):
    # Required, must be unique
    email = fields.Email(unique=True)

    # Optional (can be empty)
    phone = fields.String(max_length=20, nullable=True)

    # Has a default value
    is_active = fields.Boolean(default=True)

    # Indexed for fast lookups
    username = fields.String(max_length=50, unique=True, db_index=True)

Model Meta Options

Configure model-wide settings using the Meta class:

class Article(Model):
    title = fields.String(max_length=200)
    created_at = fields.DateTime(auto_now_add=True)

    class Meta:
        # Custom table name
        table_name = "blog_articles"

        # Default ordering (newest first)
        ordering = ["-created_at"]

        # Database indexes for performance
        indexes = [
            ("title",),  # Single column index
            ("created_at", "title"),  # Composite index
        ]

        # Unique together constraints
        unique_together = [
            ("author_id", "slug"),  # Same author can't have duplicate slugs
        ]

        # App label for admin grouping
        app_label = "blog"

Meta Options Reference

Option What It Does Example
table_name Custom database table name "blog_posts"
ordering Default sort order ["-created_at"]
indexes Database indexes [("field1", "field2")]
unique_together Multi-column uniqueness [("user_id", "slug")]
app_label Group in admin "blog"

AI Metadata

Aksara models can include metadata that helps AI agents understand your data:

class Product(Model):
    name = fields.String(
        max_length=200,
        ai_description="The product's display name shown to customers",
    )

    price = fields.Decimal(
        max_digits=10,
        decimal_places=2,
        ai_description="Price in USD, excluding tax",
    )

    internal_cost = fields.Decimal(
        max_digits=10,
        decimal_places=2,
        ai_sensitive=True,  # Hide from AI context
    )

    class Meta:
        ai_description = "Products available for purchase in the store"
Option What It Does
ai_description Human-readable description for AI
ai_sensitive Hide field from AI context
ai_agent_writable Allow/prevent AI from modifying

Relationships

Models can reference other models:

from aksara import Model, fields, CASCADE

class Author(Model):
    name = fields.String(max_length=100)

class Post(Model):
    title = fields.String(max_length=200)

    # Many posts can have one author
    author = fields.ForeignKey(Author, on_delete=CASCADE, related_name="posts")

See Relations for complete documentation.


Complete Example

from aksara import Model, fields, CASCADE
from decimal import Decimal

class Category(Model):
    """Product categories like Electronics, Clothing, etc."""

    name = fields.String(max_length=100, unique=True)
    slug = fields.String(max_length=100, unique=True)
    description = fields.Text(nullable=True)

    class Meta:
        ordering = ["name"]
        app_label = "store"


class Product(Model):
    """Items available for purchase."""

    # Basic info
    name = fields.String(max_length=200)
    slug = fields.String(max_length=200, unique=True)
    description = fields.Text()

    # Pricing
    price = fields.Decimal(max_digits=10, decimal_places=2)
    sale_price = fields.Decimal(max_digits=10, decimal_places=2, nullable=True)

    # Inventory
    sku = fields.String(max_length=50, unique=True)
    stock_quantity = fields.Integer(default=0)

    # Status
    is_active = fields.Boolean(default=True)
    is_featured = fields.Boolean(default=False)

    # Relationships
    category = fields.ForeignKey(
        Category,
        on_delete=CASCADE,
        related_name="products",
    )

    # Metadata
    tags = fields.Array(base_type="text", default=list)
    attributes = fields.JSON(default=dict)  # e.g., {"color": "red", "size": "M"}

    # Timestamps
    created_at = fields.DateTime(auto_now_add=True)
    updated_at = fields.DateTime(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        app_label = "store"
        indexes = [
            ("category_id", "is_active"),
            ("sku",),
        ]


# Using the models
async def example():
    # Create a category
    electronics = await Category.objects.create(
        name="Electronics",
        slug="electronics",
        description="Gadgets and devices",
    )

    # Create a product
    phone = await Product.objects.create(
        name="Smartphone X",
        slug="smartphone-x",
        description="Latest smartphone with amazing features",
        price=Decimal("999.99"),
        sku="PHONE-001",
        stock_quantity=100,
        category=electronics,
        tags=["smartphone", "mobile", "5g"],
        attributes={"color": "black", "storage": "256GB"},
    )

    # Query products
    active_products = await Product.objects.filter(
        is_active=True,
        stock_quantity__gt=0,
    ).order_by("-created_at")

    # Access relationship
    for product in active_products:
        category = await product.category
        print(f"{product.name} in {category.name}")

  • Fields — All field types and options
  • Querying — Filter, sort, and retrieve data
  • Relations — ForeignKey, ManyToMany, OneToOne
  • Migrations — Create and update tables