Part 3: Data Models and Serializers¶
In the previous parts, we created a project and application, and implemented a Hello World API. Now we will design data models using Tortoise ORM and create serializers that provide automatic CRUD operations.
We will design the data structure for a student course enrollment system: students, courses, and the many-to-many relationships between them.
Data Model Design¶
Business Requirements¶
Our enrollment system needs to support:
- Course Management: Create and manage course information
- Student Management: Manage student records
- Enrollment Relationships: Students can enroll in multiple courses; courses can have multiple students
- Timestamps: Track creation and update times
Defining Models¶
Edit enroll/models.py:
# enroll/models.py
from tortoise import fields
from tortoise.models import Model
class Student(Model):
class Meta:
table = "students"
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
email = fields.CharField(max_length=255, unique=True)
age = fields.IntField()
student_id = fields.CharField(max_length=20, unique=True)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
courses = fields.ManyToManyField(
"models.Course",
related_name="students",
through="student_course",
)
def __str__(self):
return f"{self.student_id} - {self.name}"
class Course(Model):
class Meta:
table = "courses"
id = fields.IntField(pk=True)
name = fields.CharField(max_length=200)
code = fields.CharField(max_length=20, unique=True)
description = fields.TextField()
credits = fields.IntField(default=3)
max_students = fields.IntField(default=50)
is_active = fields.BooleanField(default=True)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
students: fields.ManyToManyRelation["Student"]
def __str__(self):
return f"{self.code} - {self.name}"
Unfazed automatically discovers models.py in installed apps and registers them with Tortoise ORM. See Tortoise ORM for the full reference.
Key Field Types¶
| Field Type | Example | Description |
|---|---|---|
IntField(pk=True) |
id |
Primary key, auto-increment |
CharField(max_length=100) |
name |
String field with max length |
CharField(unique=True) |
email, student_id |
Unique constraint field |
TextField() |
description |
Long text field |
BooleanField(default=True) |
is_active |
Boolean with default value |
DatetimeField(auto_now_add=True) |
created_at |
Automatically set on creation |
DatetimeField(auto_now=True) |
updated_at |
Automatically set on update |
ManyToManyField() |
courses |
Many-to-many relationship |
Relationship Design¶
Student.coursesandCourse.studentsform a many-to-many relationship- The junction table
student_courseis created automatically by Tortoise ORM - Both sides support bidirectional queries (e.g.
student.courses.all(),course.students.all())
Database Configuration¶
Configure the Database Connection¶
Edit entry/settings/__init__.py to add the DATABASE configuration:
# entry/settings/__init__.py
import os
from pathlib import Path
PROJECT_DIR = Path(__file__).resolve().parent.parent
UNFAZED_SETTINGS = {
"DEBUG": True,
"PROJECT_NAME": "Tutorial Project",
"ROOT_URLCONF": "entry.routes",
"INSTALLED_APPS": [
"enroll",
],
"MIDDLEWARE": [
"unfazed.middleware.internal.common.CommonMiddleware",
],
"DATABASE": {
"CONNECTIONS": {
"default": {
"ENGINE": "tortoise.backends.sqlite",
"CREDENTIALS": {
"FILE_PATH": os.path.join(PROJECT_DIR, "db.sqlite3"),
},
}
},
},
}
See Tortoise ORM — Database Configuration for all supported engines and credentials.
Initialize and Migrate the Database¶
Unfazed uses Aerich for database migrations. Run these commands from the backend directory:
# 1. Initialize the migration directory (first time only)
unfazed-cli init-db
# 2. Generate a migration from model changes
unfazed-cli migrate --name initial
# 3. Apply the migration
unfazed-cli upgrade
See Tortoise ORM — Migration Commands for the full command reference.
Serializer Design¶
Serializers bridge the gap between database models and your API layer. They auto-generate Pydantic fields from Tortoise models and provide built-in async CRUD operations. See Serializer for the full reference.
Creating Serializers¶
Edit enroll/serializers.py:
# enroll/serializers.py
from unfazed.serializer import Serializer
from . import models as m
class StudentSerializer(Serializer):
class Meta:
model = m.Student
include = [
"id", "name", "email", "age", "student_id",
"created_at", "updated_at",
]
class CourseSerializer(Serializer):
class Meta:
model = m.Course
include = [
"id", "name", "code", "description", "credits",
"max_students", "is_active", "created_at", "updated_at",
]
class StudentWithCoursesSerializer(Serializer):
class Meta:
model = m.Student
include = [
"id", "name", "email", "age", "student_id",
"created_at", "updated_at", "courses",
]
enable_relations = True
class CourseWithStudentsSerializer(Serializer):
class Meta:
model = m.Course
include = [
"id", "name", "code", "description", "credits",
"max_students", "is_active", "created_at", "updated_at", "students",
]
enable_relations = True
Meta Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model |
Type[Model] |
required | The Tortoise ORM model to serialize. |
include |
List[str] |
[] |
Fields to include. If set, others are excluded. |
exclude |
List[str] |
[] |
Fields to exclude. Cannot be used with include. |
enable_relations |
bool |
False |
Generate fields for FK, M2M, and O2O relations. |
Using CRUD Operations¶
All CRUD methods are class methods that accept a Pydantic BaseModel as the context:
from pydantic import BaseModel
from enroll.serializers import StudentSerializer, CourseSerializer
# --- Create ---
class CreateStudent(BaseModel):
name: str
email: str
age: int
student_id: str
student = await StudentSerializer.create_from_ctx(
CreateStudent(name="Alice", email="alice@example.com", age=20, student_id="2024001")
)
# student is a StudentSerializer instance
# --- Retrieve ---
class StudentId(BaseModel):
id: int
student = await StudentSerializer.retrieve_from_ctx(StudentId(id=1))
# --- Update ---
class UpdateStudent(BaseModel):
id: int
age: int
updated = await StudentSerializer.update_from_ctx(
UpdateStudent(id=1, age=21)
)
# --- Delete ---
await StudentSerializer.destroy_from_ctx(StudentId(id=1))
# --- List with pagination ---
result = await StudentSerializer.list_from_ctx(
cond={},
page=1,
size=10,
)
# result.count — total matching records
# result.data — list of StudentSerializer instances
Relationship Data¶
To include related objects when retrieving, use a serializer with enable_relations = True:
class StudentId(BaseModel):
id: int
student_with_courses = await StudentWithCoursesSerializer.retrieve_from_ctx(
StudentId(id=1)
)
# student_with_courses.courses contains the enrolled course data
Testing the Models¶
You can verify the models interactively using the Unfazed shell:
from pydantic import BaseModel
from enroll.models import Student, Course
from enroll.serializers import StudentSerializer, CourseSerializer
class CreateCourse(BaseModel):
name: str
code: str
description: str
credits: int
max_students: int
class CreateStudent(BaseModel):
name: str
email: str
age: int
student_id: str
# Create a course
course = await CourseSerializer.create_from_ctx(
CreateCourse(
name="Python Programming",
code="CS101",
description="Learn the basics of Python",
credits=3,
max_students=30,
)
)
# Create a student
student = await StudentSerializer.create_from_ctx(
CreateStudent(
name="Alice",
email="alice@example.com",
age=20,
student_id="2024001",
)
)
# Enroll the student in the course (via model instances)
student_instance = await Student.get(id=student.id)
course_instance = await Course.get(id=course.id)
await student_instance.courses.add(course_instance)
# Verify the relationship
enrolled_courses = await student_instance.courses.all()
course_students = await course_instance.students.all()
print(f"Student {student.name} enrolled in {len(enrolled_courses)} courses")
print(f"Course {course.name} has {len(course_students)} students")
Next Steps¶
You have designed data models and serializers with built-in CRUD operations. In the next part, we will:
- Design API endpoints with typed parameter annotations
- Define request/response schemas for OpenAPI documentation
- Learn Unfazed's parameter system (
Path,Query,Json, etc.)
Continue to Part 4: API Interface Design and Schema Definition.