Pydantic Examples

See Pydantic serialisation

Basic Pydantic

"""
This example demonstrates pydantic serialisation
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
from tortoise.models import Model


class Tournament(Model):
    id = fields.IntField(primary_key=True)
    name = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)

    events: fields.ReverseRelation["Event"]

    class Meta:
        ordering = ["name"]


class Event(Model):
    id = fields.IntField(primary_key=True)
    name = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)
    tournament: fields.ForeignKeyNullableRelation[Tournament] = fields.ForeignKeyField(
        Tournament, related_name="events", null=True
    )
    participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
        "models.Team", related_name="events", through="event_team"
    )
    address: fields.OneToOneNullableRelation["Address"]

    class Meta:
        ordering = ["name"]


class Address(Model):
    city = fields.CharField(max_length=64)
    street = fields.CharField(max_length=128)
    created_at = fields.DatetimeField(auto_now_add=True)

    event: fields.OneToOneRelation[Event] = fields.OneToOneField(
        Event, on_delete=fields.OnDelete.CASCADE, related_name="address", primary_key=True
    )

    class Meta:
        ordering = ["city"]


class Team(Model):
    id = fields.IntField(primary_key=True)
    name = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)

    events: fields.ManyToManyRelation[Event]

    class Meta:
        ordering = ["name"]


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()
    Event_Pydantic = pydantic_model_creator(Event)
    Event_Pydantic_List = pydantic_queryset_creator(Event)
    Tournament_Pydantic = pydantic_model_creator(Tournament)
    Team_Pydantic = pydantic_model_creator(Team)

    # print(Event_Pydantic_List.schema_json(indent=4))
    # print(Event_Pydantic.schema_json(indent=4))
    # print(Tournament_Pydantic.schema_json(indent=4))
    # print(Team_Pydantic.schema_json(indent=4))

    tournament = await Tournament.create(name="New Tournament")
    tournament2 = await Tournament.create(name="Old Tournament")
    await Event.create(name="Empty")
    event = await Event.create(name="Test", tournament=tournament)
    event2 = await Event.create(name="TestLast", tournament=tournament)
    event3 = await Event.create(name="Test2", tournament=tournament2)
    await Address.create(city="Santa Monica", street="Ocean", event=event)
    await Address.create(city="Somewhere Else", street="Lane", event=event2)
    team1 = await Team.create(name="Onesies")
    team2 = await Team.create(name="T-Shirts")
    team3 = await Team.create(name="Alternates")
    await event.participants.add(team1, team2, team3)
    await event2.participants.add(team1, team2)
    await event3.participants.add(team1, team3)

    p = await Event_Pydantic.from_tortoise_orm(await Event.get(name="Test"))
    print("One Event:", p.model_dump_json(indent=4))

    p = await Tournament_Pydantic.from_tortoise_orm(await Tournament.get(name="New Tournament"))
    print("One Tournament:", p.model_dump_json(indent=4))

    p = await Team_Pydantic.from_tortoise_orm(await Team.get(name="Onesies"))
    print("One Team:", p.model_dump_json(indent=4))

    pl = await Event_Pydantic_List.from_queryset(Event.filter(address__event_id__isnull=True))
    print("All Events without addresses:", pl.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())

Early model Init

"""
This example demonstrates pydantic serialisation, and how to use early partial init.
"""

from tortoise import Tortoise, fields
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.models import Model


class Tournament(Model):
    id = fields.IntField(primary_key=True)
    name = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)

    events: fields.ReverseRelation["Event"]

    class Meta:
        ordering = ["name"]


class Event(Model):
    id = fields.IntField(primary_key=True)
    name = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)
    tournament: fields.ForeignKeyNullableRelation[Tournament] = fields.ForeignKeyField(
        Tournament, related_name="events", null=True
    )

    class Meta:
        ordering = ["name"]


Event_TooEarly = pydantic_model_creator(Event)
print("Relations are missing if models not initialized:")
print(Event_TooEarly.schema_json(indent=4))


Tortoise.init_models(["__main__"], "models")

Event_Pydantic = pydantic_model_creator(Event)
print("\nRelations are now present:")
print(Event_Pydantic.schema_json(indent=4))

# Now we can use the pydantic model early if needed

Computed fields & Nullable relations

"""
Pydantic computed fields example

Here we demonstrate:
* Nullable FK fields become Optional in the Pydantic schema (with ``"default": null``).
* Computed fields that access reverse relations work when the relation is included.
* Computed fields still work when the relation is excluded but manually prefetched.
* NoValuesFetched error propagation when a computed field accesses an unfetched relation.
* Graceful handling pattern using try/except inside the computed function.
"""

import json

from pydantic_core import PydanticSerializationError

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.exceptions import NoValuesFetched
from tortoise.models import Model


class Department(Model):
    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)

    # Define reverse relation for type checking and auto completion
    employees: fields.ReverseRelation["Employee"]

    def employee_count(self) -> int:
        """
        Counts employees in the department.

        Uses try/except to gracefully handle the case where the relation has not been
        fetched, returning 0 instead of raising an error.
        """
        try:
            return len(self.employees)
        except (NoValuesFetched, AttributeError):
            return 0

    def employee_names(self) -> str:
        """
        Returns a comma-separated list of employee names.

        Does NOT handle NoValuesFetched -- demonstrates error propagation when the
        relation has not been fetched.
        """
        return ", ".join(e.name for e in self.employees)

    class PydanticMeta:
        computed = ("employee_count", "employee_names")


class Employee(Model):
    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)

    department: fields.ForeignKeyNullableRelation[Department] = fields.ForeignKeyField(
        "models.Department", related_name="employees", null=True
    )


# Initialise model structure early so we can create pydantic models at module level
Tortoise.init_models(["__main__"], "models")


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    # Create test data
    engineering = await Department.create(name="Engineering")
    await Employee.create(name="Alice", department=engineering)
    await Employee.create(name="Bob", department=engineering)
    await Employee.create(name="Charlie")  # no department (nullable FK)

    # ──────────────────────────────────────────────────────────────────────
    # Section 1: Nullable FK is Optional in the Pydantic schema
    #
    # A nullable ForeignKeyField generates a pydantic field with
    # "default": null that is NOT in "required".
    # ──────────────────────────────────────────────────────────────────────
    print("=" * 70)
    print("Section 1: Nullable FK is Optional")
    print("=" * 70)

    Employee_Pydantic = pydantic_model_creator(Employee)
    schema = Employee_Pydantic.model_json_schema()
    print(json.dumps(schema, indent=4))

    # The 'department' field has "default": null and is NOT in "required"
    required = schema.get("required", [])
    print(f"\nRequired fields: {required}")
    print("'department' in required:", "department" in required)

    dept_props = schema.get("properties", {}).get("department", {})
    has_default_null = dept_props.get("default") is None and "default" in dept_props
    print(f"'department' has default null: {has_default_null}")

    # ──────────────────────────────────────────────────────────────────────
    # Section 2: Computed field with included relation (auto-prefetched)
    #
    # When the 'employees' reverse relation is included in the pydantic
    # model, from_tortoise_orm() auto-prefetches it. Both computed fields
    # work because the relation data is available.
    # ──────────────────────────────────────────────────────────────────────
    print("\n" + "=" * 70)
    print("Section 2: Computed field with included relation")
    print("=" * 70)

    Department_Pydantic = pydantic_model_creator(Department)

    dept = await Department.get(name="Engineering")
    dept_pydantic = await Department_Pydantic.from_tortoise_orm(dept)
    print(dept_pydantic.model_dump_json(indent=4))

    # Both computed fields work because the relation was auto-prefetched
    print(f"\nemployee_count: {dept_pydantic.employee_count}")
    print(f"employee_names: {dept_pydantic.employee_names}")

    # ──────────────────────────────────────────────────────────────────────
    # Section 3: Computed field with excluded relation + manual prefetch
    #
    # The 'employees' relation is excluded from the pydantic model (so it
    # won't appear in the output), but we manually prefetch it before
    # serialization so the computed fields can still access the data.
    # ──────────────────────────────────────────────────────────────────────
    print("\n" + "=" * 70)
    print("Section 3: Excluded relation + manual prefetch")
    print("=" * 70)

    Department_Pydantic_NoEmployees = pydantic_model_creator(
        Department,
        name="Department_NoEmployees",
        exclude=("employees",),
    )

    dept = await Department.get(name="Engineering")
    # Manually prefetch the relation so computed fields can access it
    await dept.fetch_related("employees")
    dept_pydantic = await Department_Pydantic_NoEmployees.from_tortoise_orm(dept)
    print(dept_pydantic.model_dump_json(indent=4))

    print(f"\nemployee_count: {dept_pydantic.employee_count}")
    print(f"employee_names: {dept_pydantic.employee_names}")

    # ──────────────────────────────────────────────────────────────────────
    # Section 4: NoValuesFetched error propagation
    #
    # Same excluded model but WITHOUT manual prefetch. The employee_names()
    # computed field does not handle NoValuesFetched internally, so the
    # wrapper in creator.py re-raises with a descriptive message during
    # serialization.
    # ──────────────────────────────────────────────────────────────────────
    print("\n" + "=" * 70)
    print("Section 4: NoValuesFetched error propagation")
    print("=" * 70)

    dept = await Department.get(name="Engineering")
    dept_pydantic = await Department_Pydantic_NoEmployees.from_tortoise_orm(dept)
    try:
        # Serialization triggers the computed field, which raises NoValuesFetched
        dept_pydantic.model_dump_json(indent=4)
    except (PydanticSerializationError, NoValuesFetched) as e:
        print(f"Caught error during serialization: {e}")

    # ──────────────────────────────────────────────────────────────────────
    # Section 5: Graceful handling pattern
    #
    # A model with only the graceful computed field (employee_count) that
    # handles NoValuesFetched internally. Even without prefetching, it
    # returns 0 instead of crashing. Contrast with employee_names which
    # would fail in the same scenario.
    # ──────────────────────────────────────────────────────────────────────
    print("\n" + "=" * 70)
    print("Section 5: Graceful handling pattern")
    print("=" * 70)

    # Override PydanticMeta so only the graceful computed field is included
    class GracefulMeta:
        computed = ("employee_count",)

    Department_Pydantic_GracefulOnly = pydantic_model_creator(
        Department,
        name="Department_GracefulOnly",
        exclude=("employees",),
        meta_override=GracefulMeta,
    )

    dept = await Department.get(name="Engineering")
    # No manual prefetch -- employee_count() handles the exception internally
    dept_pydantic = await Department_Pydantic_GracefulOnly.from_tortoise_orm(dept)
    print(dept_pydantic.model_dump_json(indent=4))

    # employee_count returns 0 gracefully instead of crashing
    print(f"\nemployee_count (graceful, no prefetch): {dept_pydantic.employee_count}")
    print(
        "employee_names would raise NoValuesFetched in the same scenario, "
        "but employee_count handles it and returns 0."
    )


if __name__ == "__main__":
    run_async(run())

Recursive models + Computed fields

"""
This example demonstrates pydantic serialisation of a recursively cycled model.
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.exceptions import NoValuesFetched
from tortoise.models import Model


class Employee(Model):
    name = fields.CharField(max_length=50)

    manager: fields.ForeignKeyNullableRelation["Employee"] = fields.ForeignKeyField(
        "models.Employee", related_name="team_members", null=True
    )
    team_members: fields.ReverseRelation["Employee"]

    talks_to: fields.ManyToManyRelation["Employee"] = fields.ManyToManyField(
        "models.Employee", related_name="gets_talked_to"
    )
    gets_talked_to: fields.ManyToManyRelation["Employee"]

    def name_length(self) -> int:
        # Computes length of name
        # Note that this function needs to be annotated with a return type so that pydantic
        # can generate a valid schema
        return len(self.name)

    def team_size(self) -> int:
        """
        Computes team size.

        Note that this function needs to be annotated with a return type so that pydantic can
         generate a valid schema.

        Note that the pydantic serializer can't call async methods, but the tortoise helpers
         pre-fetch relational data, so that it is available before serialization. So we don't
         need to await the relation. We do however have to protect against the case where no
         prefetching was done, hence catching and handling the
         ``tortoise.exceptions.NoValuesFetched`` exception.
        """
        try:
            return len(self.team_members)
        except NoValuesFetched:
            return -1

    def not_annotated(self):
        # Never called due to no annotation!
        raise NotImplementedError("Not Done")

    class PydanticMeta:
        computed = ["name_length", "team_size", "not_annotated"]
        exclude = ["manager", "gets_talked_to"]
        allow_cycles = True
        max_recursion = 4


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    Employee_Pydantic = pydantic_model_creator(Employee)
    # print(Employee_Pydantic.schema_json(indent=4))

    root = await Employee.create(name="Root")
    loose = await Employee.create(name="Loose")
    _1 = await Employee.create(name="1. First H1", manager=root)
    _2 = await Employee.create(name="2. Second H1", manager=root)
    _1_1 = await Employee.create(name="1.1. First H2", manager=_1)
    _1_1_1 = await Employee.create(name="1.1.1. First H3", manager=_1_1)
    _2_1 = await Employee.create(name="2.1. Second H2", manager=_2)
    _2_2 = await Employee.create(name="2.2. Third H2", manager=_2)

    await _1.talks_to.add(_2, _1_1_1, loose)
    await _2_1.gets_talked_to.add(_2_2, _1_1, loose)

    p = await Employee_Pydantic.from_tortoise_orm(await Employee.get(name="Root"))
    print(p.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())

Tutorial sources

1: Basic usage

"""
Pydantic tutorial 1

Here we introduce:
* Creating a Pydantic model from a Tortoise model
* Docstrings & doc-comments are used
* Evaluating the generated schema
* Simple serialisation with both .model_dump() and .model_dump_json()
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.models import Model


class Tournament(Model):
    """
    This references a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    #: The date-time the Tournament record was created at
    created_at = fields.DatetimeField(auto_now_add=True)


Tournament_Pydantic = pydantic_model_creator(Tournament)
# Print JSON-schema
print(Tournament_Pydantic.schema_json(indent=4))


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    # Create object
    tournament = await Tournament.create(name="New Tournament")
    # Serialise it
    tourpy = await Tournament_Pydantic.from_tortoise_orm(tournament)

    # As Python dict with Python objects (e.g. datetime)
    print(tourpy.model_dump())
    # As serialised JSON (e.g. datetime is ISO8601 string representation)
    print(tourpy.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())

2: Querysets & Lists

"""
Pydantic tutorial 2

Here we introduce:
* Creating a list-model to serialise a queryset
* Default sorting is honoured
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_queryset_creator
from tortoise.models import Model


class Tournament(Model):
    """
    This references a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    #: The date-time the Tournament record was created at
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        # Define the default ordering
        #  the pydantic serialiser will use this to order the results
        ordering = ["name"]


# Create a list of models for population from a queryset.
Tournament_Pydantic_List = pydantic_queryset_creator(Tournament)
# Print JSON-schema
print(Tournament_Pydantic_List.schema_json(indent=4))


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    # Create objects
    await Tournament.create(name="New Tournament")
    await Tournament.create(name="Another")
    await Tournament.create(name="Last Tournament")

    # Serialise it
    tourpy = await Tournament_Pydantic_List.from_queryset(Tournament.all())

    # As Python dict with Python objects (e.g. datetime)
    # Note that the root element is 'root' that contains the root element.
    print(tourpy.model_dump())
    # As serialised JSON (e.g. datetime is ISO8601 string representation)
    print(tourpy.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())

3: Relations & Early-init

"""
Pydantic tutorial 3

Here we introduce:
* Relationships
* Early model init
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.models import Model


class Tournament(Model):
    """
    This references a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    #: The date-time the Tournament record was created at
    created_at = fields.DatetimeField(auto_now_add=True)


class Event(Model):
    """
    This references an Event in a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    created_at = fields.DatetimeField(auto_now_add=True)

    tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
        Tournament, related_name="events", description="The Tournament this happens in"
    )


# Early model, does not include relations
Tournament_Pydantic_Early = pydantic_model_creator(Tournament)
# Print JSON-schema
print(Tournament_Pydantic_Early.schema_json(indent=4))


# Initialise model structure early. This does not init any database structures
Tortoise.init_models(["__main__"], "models")


# We now have a complete model
Tournament_Pydantic = pydantic_model_creator(Tournament)
# Print JSON-schema
print(Tournament_Pydantic.schema_json(indent=4))

# Note how both schema's don't follow relations back.
Event_Pydantic = pydantic_model_creator(Event)
# Print JSON-schema
print(Event_Pydantic.schema_json(indent=4))


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    # Create objects
    tournament = await Tournament.create(name="New Tournament")
    event = await Event.create(name="The Event", tournament=tournament)

    # Serialise Tournament
    tourpy = await Tournament_Pydantic.from_tortoise_orm(tournament)

    # As serialised JSON
    print(tourpy.model_dump_json(indent=4))

    # Serialise Event
    eventpy = await Event_Pydantic.from_tortoise_orm(event)

    # As serialised JSON
    print(eventpy.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())

4: PydanticMeta & Callables

"""
Pydantic tutorial 4

Here we introduce:
* Configuring model creator via PydanticMeta class.
* Using callable functions to annotate extra data.
"""

from tortoise import Tortoise, fields, run_async
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.exceptions import NoValuesFetched
from tortoise.models import Model


class Tournament(Model):
    """
    This references a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    created_at = fields.DatetimeField(auto_now_add=True)

    # It is useful to define the reverse relations manually so that type checking
    #  and auto completion work
    events: fields.ReverseRelation["Event"]

    def name_length(self) -> int:
        """
        Computes length of name
        """
        # Note that this function needs to be annotated with a return type so that pydantic
        #  can generate a valid schema
        return len(self.name)

    def events_num(self) -> int:
        """
        Computes team size.
        """
        # Note that this function needs to be annotated with a return type so that pydantic
        #  can generate a valid schema.

        # Note that the pydantic serializer can't call async methods, but the tortoise helpers
        #  pre-fetch relational data, so that it is available before serialization. So we don't
        #  need to await the relation. We do however have to protect against the case where no
        #  prefetching was done, hence catching and handling the
        #  ``tortoise.exceptions.NoValuesFetched`` exception
        try:
            return len(self.events)
        except NoValuesFetched:
            return -1

    class PydanticMeta:
        # Let's exclude the created timestamp
        exclude = ("created_at",)
        # Let's include two callables as computed columns
        computed = ("name_length", "events_num")


class Event(Model):
    """
    This references an Event in a Tournament
    """

    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=100)
    created_at = fields.DatetimeField(auto_now_add=True)

    tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
        Tournament, related_name="events", description="The Tournament this happens in"
    )

    class Meta:
        ordering = ["name"]

    class PydanticMeta:
        exclude = ("created_at",)


# Initialise model structure early. This does not init any database structures
Tortoise.init_models(["__main__"], "models")
Tournament_Pydantic = pydantic_model_creator(Tournament)


# Print JSON-schema
print(Tournament_Pydantic.schema_json(indent=4))


async def run():
    await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
    await Tortoise.generate_schemas()

    # Create objects
    tournament = await Tournament.create(name="New Tournament")
    await Event.create(name="Event 1", tournament=tournament)
    await Event.create(name="Event 2", tournament=tournament)

    # Serialise Tournament
    tourpy = await Tournament_Pydantic.from_tortoise_orm(tournament)

    # As serialised JSON
    print(tourpy.model_dump_json(indent=4))


if __name__ == "__main__":
    run_async(run())