{"id":13688733,"url":"https://github.com/allmonday/pydantic-resolve","last_synced_at":"2026-03-02T16:06:01.228Z","repository":{"id":121567728,"uuid":"610649159","full_name":"allmonday/pydantic-resolve","owner":"allmonday","description":"pydantic-resolve is a Pydantic-based data construction tool that enables you to assemble complex data structures declaratively instead of writing boring imperative glue code.","archived":false,"fork":false,"pushed_at":"2026-02-04T00:48:03.000Z","size":3773,"stargazers_count":307,"open_issues_count":2,"forks_count":11,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-04T12:53:21.297Z","etag":null,"topics":["bff","fastapi","fullstack","graphql","pydantic","python"],"latest_commit_sha":null,"homepage":"https://allmonday.github.io/pydantic-resolve","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/allmonday.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-03-07T07:46:38.000Z","updated_at":"2026-02-04T00:47:48.000Z","dependencies_parsed_at":"2023-09-29T06:47:16.976Z","dependency_job_id":"c3d62574-5777-4c0f-a7f4-30ba5ec91578","html_url":"https://github.com/allmonday/pydantic-resolve","commit_stats":null,"previous_names":["allmonday/pydantic_resolve"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/allmonday/pydantic-resolve","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fpydantic-resolve","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fpydantic-resolve/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fpydantic-resolve/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fpydantic-resolve/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/allmonday","download_url":"https://codeload.github.com/allmonday/pydantic-resolve/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fpydantic-resolve/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29674496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T03:11:15.450Z","status":"ssl_error","status_checked_at":"2026-02-21T03:10:34.920Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bff","fastapi","fullstack","graphql","pydantic","python"],"created_at":"2024-08-02T15:01:21.407Z","updated_at":"2026-02-21T05:05:22.795Z","avatar_url":"https://github.com/allmonday.png","language":"Python","funding_links":[],"categories":["Python","Utilities","Third-Party Extensions"],"sub_categories":["Utils"],"readme":"# Pydantic Resolve\n\n\n\u003e A tool for building Domain layer modeling and use case assembly.\n\n\n[![pypi](https://img.shields.io/pypi/v/pydantic-resolve.svg)](https://pypi.python.org/pypi/pydantic-resolve)\n[![PyPI Downloads](https://static.pepy.tech/badge/pydantic-resolve/month)](https://pepy.tech/projects/pydantic-resolve)\n![Python Versions](https://img.shields.io/pypi/pyversions/pydantic-resolve)\n[![CI](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml/badge.svg)](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml)\n\n[中文版](./README.zh.md)\n\n## What is this?\n\n**pydantic-resolve** is a Pydantic-based data construction tool that enables you to assemble complex data structures **declaratively** without writing boring imperative glue code.\n\n### What problem does it solve?\n\nConsider this scenario: you need to provide API data to frontend clients from multiple data sources (databases, RPC services, etc.) that requires composition, transformation, and computation. How would you typically approach this?\n\nFirst, let's define the response schemas:\n\n```python\nfrom pydantic import BaseModel\nfrom typing import Optional, List\n\nclass UserResponse(BaseModel):\n    id: int\n    name: str\n    email: str\n\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n    owner_id: int\n    owner: Optional[UserResponse] = None\n\nclass SprintResponse(BaseModel):\n    id: int\n    name: str\n    tasks: List[TaskResponse] = []\n\nclass TeamResponse(BaseModel):\n    id: int\n    name: str\n    sprints: List[SprintResponse] = []\n    total_tasks: int = 0\n```\n\nNow, let's see how to populate these schemas with data:\n\n```python\n# Traditional approach: imperative data assembly with Pydantic schemas\nasync def get_teams_with_detail(session):\n    # 1. Fetch team list from database\n    teams_data = await session.execute(select(Team))\n    teams_data = teams_data.scalars().all()\n\n    # 2. Build response objects and fetch related data imperatively\n    teams = []\n    for team_data in teams_data:\n        team = TeamResponse(**team_data.__dict__)\n\n        # Fetch sprints for this team\n        sprints_data = await get_sprints_by_team(session, team.id)\n        team.sprints = []\n\n        for sprint_data in sprints_data:\n            sprint = SprintResponse(**sprint_data.__dict__)\n\n            # Fetch tasks for this sprint\n            tasks_data = await get_tasks_by_sprint(session, sprint.id)\n            sprint.tasks = []\n\n            for task_data in tasks_data:\n                task = TaskResponse(**task_data.__dict__)\n\n                # Fetch owner for this task\n                owner_data = await get_user_by_id(session, task.owner_id)\n                task.owner = UserResponse(**owner_data.__dict__)\n\n                sprint.tasks.append(task)\n\n            team.sprints.append(sprint)\n\n        # Calculate statistics\n        team.total_tasks = sum(len(sprint.tasks) for sprint in team.sprints)\n        teams.append(team)\n\n    return teams\n```\n\n**Problems**:\n- Extensive nested loops\n- N+1 query problem (poor performance)\n- Difficult to maintain and extend\n- Data fetching logic mixed with business logic\n\n**The pydantic-resolve approach**:\n\n```python\n# Declarative: describe what you want, not how to do it\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n    owner_id: int\n\n    owner: Optional[UserResponse] = None\n    def resolve_owner(self, loader=Loader(user_batch_loader)):\n        return loader.load(self.owner_id)\n\nclass SprintResponse(BaseModel):\n    id: int\n    name: str\n\n    tasks: list[TaskResponse] = []\n    def resolve_tasks(self, loader=Loader(sprint_to_tasks_loader)):\n        return loader.load(self.id)\n\nclass TeamResponse(BaseModel):\n    id: int\n    name: str\n\n    sprints: list[SprintResponse] = []\n    def resolve_sprints(self, loader=Loader(team_to_sprints_loader)):\n        return loader.load(self.id)\n\n    # Calculate statistics automatically after sprints are loaded\n    total_tasks: int = 0\n    def post_total_tasks(self):\n        return sum(len(sprint.tasks) for sprint in self.sprints)\n\n# Usage\nteams = await query_teams_from_db(session)\nresult = await Resolver().resolve(teams)\n```\n\n**Advantages**:\n- Automatic batch loading (using DataLoader pattern)\n- No N+1 query problem\n- Clear separation of data fetching logic\n- Easy to extend and maintain\n\n### Core Features\n\n- **Declarative data composition**: Declare how to fetch related data via `resolve_{field}` methods\n- **Automatic batch loading**: Built-in DataLoader automatically batches queries to avoid N+1 issues\n- **Data post-processing**: Transform and compute data after fetching via `post_{field}` methods\n- **Cross-layer data passing**: Parent nodes can expose data to descendants, children can collect data to parents\n- **Entity Relationship Diagram (ERD)**: Define entity relationships and auto-generate resolution logic\n- **Framework integration**: Seamless integration with FastAPI, Litestar, Django Ninja\n\n## Quick Start\n\n### Installation\n\n```bash\npip install pydantic-resolve\n```\n\n\u003e Note: pydantic-resolve v2+ only supports Pydantic v2\n\n### Step 1: Define Data Loaders\n\nFirst, you need to define batch data loaders (this is the Python implementation of Facebook's DataLoader pattern):\n\n```python\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy import select\nfrom pydantic_resolve import build_list\n\n# Batch fetch users\nasync def batch_get_users(session: AsyncSession, user_ids: list[int]):\n    result = await session.execute(select(User).where(User.id.in_(user_ids)))\n    return result.scalars().all()\n\n# User loader\nasync def user_batch_loader(user_ids: list[int]):\n    async with get_db_session() as session:\n        users = await batch_get_users(session, user_ids)\n        # Map user list to corresponding IDs\n        return build_list(users, user_ids, lambda u: u.id)\n\n# Batch fetch team tasks\nasync def batch_get_tasks_by_team(session: AsyncSession, team_ids: list[int]):\n    result = await session.execute(select(Task).where(Task.team_id.in_(team_ids)))\n    return result.scalars().all()\n\n# Team task loader\nasync def team_to_tasks_loader(team_ids: list[int]):\n    async with get_db_session() as session:\n        tasks = await batch_get_tasks_by_team(session, team_ids)\n        return build_list(tasks, team_ids, lambda t: t.team_id)\n```\n\n### Step 2: Define Response Models\n\nUse Pydantic BaseModel to define response structures and declare how to fetch related data via `resolve_` prefixed methods:\n\n```python\nfrom typing import Optional, List\nfrom pydantic import BaseModel\nfrom pydantic_resolve import Resolver, Loader\n\nclass UserResponse(BaseModel):\n    id: int\n    name: str\n    email: str\n\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n    owner_id: int\n\n    # Declaration: fetch owner via owner_id\n    owner: Optional[UserResponse] = None\n    def resolve_owner(self, loader=Loader(user_batch_loader)):\n        return loader.load(self.owner_id)\n\nclass TeamResponse(BaseModel):\n    id: int\n    name: str\n\n    # Declaration: fetch all tasks for this team via team_id\n    tasks: List[TaskResponse] = []\n    def resolve_tasks(self, loader=Loader(team_to_tasks_loader)):\n        return loader.load(self.id)\n```\n\n### Step 3: Use Resolver to Resolve Data\n\n```python\nfrom fastapi import FastAPI, Depends\n\napp = FastAPI()\n\n@app.get(\"/teams\", response_model=List[TeamResponse])\nasync def get_teams():\n    # 1. Fetch base data from database (multiple teams)\n    teams_data = await get_teams_from_db()\n\n    # 2. Convert to Pydantic models\n    teams = [TeamResponse.model_validate(t) for t in teams_data]\n\n    # 3. Resolve all related data\n    result = await Resolver().resolve(teams)\n\n    return result\n```\n\nThat's it! Resolver will automatically:\n1. Discover all `resolve_` methods\n2. **Collect all task IDs needed by teams** (e.g., 3 teams require 3 task fetches)\n3. **Batch call the corresponding loader** (one query to load all tasks instead of 3)\n4. Populate results to corresponding fields\n\n**The power of DataLoader**:\n```python\n# Assume 3 teams, each with multiple tasks\n# Traditional approach: 3 queries\nSELECT * FROM tasks WHERE team_id = 1\nSELECT * FROM tasks WHERE team_id = 2\nSELECT * FROM tasks WHERE team_id = 3\n\n# DataLoader approach: 1 query\nSELECT * FROM tasks WHERE team_id IN (1, 2, 3)\n```\n\n## Core Concepts Deep Dive\n\n### DataLoader: The Secret Weapon for Batch Loading\n\n**Problem**: Traditional related data loading leads to N+1 queries\n\n```python\n# Wrong example: N+1 queries\nfor task in tasks:\n    task.owner = await get_user_by_id(task.owner_id)  # Generates N queries\n```\n\n**Solution**: DataLoader batch loading\n\n```python\n# DataLoader automatically batches requests\ntasks = [Task1(owner_id=1), Task2(owner_id=2), Task3(owner_id=1)]\n\n# DataLoader will merge these requests into one query:\n# SELECT * FROM users WHERE id IN (1, 2)\n```\n\n### resolve Methods: Declare Data Dependencies\n\n`resolve_{field_name}` methods are used to declare how to fetch data for that field:\n\n```python\nclass CommentResponse(BaseModel):\n    id: int\n    content: str\n    author_id: int\n\n    # Resolver will automatically call this method and assign the return value to author field\n    author: Optional[UserResponse] = None\n    def resolve_author(self, loader=Loader(user_batch_loader)):\n        return loader.load(self.author_id)\n```\n\n### post Methods: Data Post-Processing\n\nAfter all `resolve_` methods complete execution, `post_{field_name}` methods are called. This can be used for:\n\n- Computing derived fields\n- Formatting data\n- Aggregating child node data\n\n```python\nclass SprintResponse(BaseModel):\n    id: int\n    name: str\n\n    tasks: List[TaskResponse] = []\n    def resolve_tasks(self, loader=Loader(sprint_to_tasks_loader)):\n        return loader.load(self.id)\n\n    # After tasks are loaded, calculate total task count\n    total_tasks: int = 0\n    def post_total_tasks(self):\n        return len(self.tasks)\n\n    # Calculate sum of all task estimates\n    total_estimate: int = 0\n    def post_total_estimate(self):\n        return sum(task.estimate for task in self.tasks)\n```\n\n### Cross-Layer Data Passing\n\n**Scenario**: Child nodes need to access parent node data, or parent nodes need to collect child node data\n\n#### Expose: Parent Nodes Expose Data to Child Nodes\n\n```python\nfrom pydantic_resolve import ExposeAs\n\nclass StoryResponse(BaseModel):\n    id: int\n    name: Annotated[str, ExposeAs('story_name')]  # Expose to child nodes\n\n    tasks: List[TaskResponse] = []\n\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n\n    # Both post/resolve methods can access data exposed by ancestor nodes\n    full_name: str = \"\"\n    def post_full_name(self, ancestor_context):\n        # Get parent (Story) name\n        story_name = ancestor_context.get('story_name')\n        return f\"{story_name} - {self.name}\"\n```\n\n#### Collect: Child Nodes Send Data to Parent Nodes\n\n```python\nfrom pydantic_resolve import Collector, SendTo\n\nclass TaskResponse(BaseModel):\n    id: int\n    owner_id: int\n\n    # Load owner data and send to parent's related_users collector\n    owner: Annotated[Optional[UserResponse], SendTo('related_users')] = None\n    def resolve_owner(self, loader=Loader(user_batch_loader)):\n        return loader.load(self.owner_id)\n\nclass StoryResponse(BaseModel):\n    id: int\n    name: str\n\n    tasks: List[TaskResponse] = []\n    def resolve_tasks(self, loader=Loader(story_to_tasks_loader)):\n        return loader.load(self.id)\n\n    # Collect all child node owners\n    related_users: List[UserResponse] = []\n    def post_related_users(self, collector=Collector(alias='related_users')):\n        return collector.values()\n```\n\n## Advanced Usage\n\n### Using Entity Relationship Diagram (ERD)\n\nFor complex applications, you can define entity relationships at the application level and automatically generate resolution logic:\n\n```python\nfrom pydantic_resolve import base_entity, Relationship, LoadBy, config_global_resolver\n\n# 1. Define base entities\nBaseEntity = base_entity()\n\nclass Story(BaseModel, BaseEntity):\n    __relationships__ = [\n        # Define relationship: load all tasks for this story via id field\n        Relationship(field='id', target_kls=list['Task'], loader=story_to_tasks_loader),\n        # Define relationship: load owner via owner_id field\n        Relationship(field='owner_id', target_kls='User', loader=user_batch_loader),\n    ]\n\n    id: int\n    name: str\n    owner_id: int\n    sprint_id: int\n\nclass Task(BaseModel, BaseEntity):\n    __relationships__ = [\n        Relationship(field='owner_id', target_kls='User', loader=user_batch_loader),\n    ]\n\n    id: int\n    name: str\n    owner_id: int\n    story_id: int\n    estimate: int\n\nclass User(BaseModel):\n    id: int\n    name: str\n    email: str\n\n# 2. Generate ER diagram and register to global Resolver\ndiagram = BaseEntity.get_diagram()\nconfig_global_resolver(diagram)\n\n# 3. When defining response models, no need to write resolve methods\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n    owner_id: int\n\n    # LoadBy automatically finds relationship definitions in ERD\n    owner: Annotated[Optional[User], LoadBy('owner_id')] = None\n\nclass StoryResponse(BaseModel):\n    id: int\n    name: str\n\n    tasks: Annotated[List[TaskResponse], LoadBy('id')] = []\n    owner: Annotated[Optional[User], LoadBy('owner_id')] = None\n\n# 4. Use directly\nstories = await query_stories_from_db(session)\nresult = await Resolver().resolve(stories)\n```\n\nAdvantages:\n- Centralized relationship definition management\n- More concise response models\n- Type-safe\n- Visualizable dependencies (with fastapi-voyager)\n\n### Defining Data Subsets\n\nIf you only want to return a subset of entity fields, you can use `DefineSubset`:\n\n```python\nfrom pydantic_resolve import DefineSubset\n\n# Assume you have a complete User model\nclass FullUser(BaseModel):\n    id: int\n    name: str\n    email: str\n    password_hash: str\n    created_at: datetime\n    updated_at: datetime\n\n# Select only required fields\nclass UserSummary(DefineSubset):\n    __subset__ = (FullUser, ('id', 'name', 'email'))\n\n# Auto-generates:\n# class UserSummary(BaseModel):\n#     id: int\n#     name: str\n#     email: str\n```\n\n### Advanced Subset Configuration: SubsetConfig\n\nFor more complex configurations (like exposing fields to child nodes simultaneously), use `SubsetConfig`:\n\n```python\nfrom pydantic_resolve import DefineSubset, SubsetConfig\n\nclass StoryResponse(DefineSubset):\n    __subset__ = SubsetConfig(\n        kls=StoryEntity,              # Source model\n        fields=['id', 'name', 'owner_id'],  # Fields to include\n        expose_as=[('name', 'story_name')],  # Alias exposed to child nodes\n        send_to=[('id', 'story_id_collector')]  # Send to collector\n    )\n\n# Equivalent to:\n# class StoryResponse(BaseModel):\n#     id: Annotated[int, SendTo('story_id_collector')]\n#     name: Annotated[str, ExposeAs('story_name')]\n#     owner_id: int\n#\n```\n\n## Performance Optimization Tips\n\n### Database Session Management\n\nWhen using FastAPI + SQLAlchemy, pay attention to session lifecycle:\n\n```python\n@router.get(\"/teams\", response_model=List[TeamResponse])\nasync def get_teams(session: AsyncSession = Depends(get_session)):\n    # 1. Fetch base data (multiple teams)\n    teams = await get_teams_from_db(session)\n\n    # 2. Release session immediately (avoid deadlock)\n    await session.close()\n\n    # 3. Loaders inside Resolver will create new sessions\n    teams = [TeamResponse.model_validate(t) for t in teams]\n    result = await Resolver().resolve(teams)\n\n    return result\n```\n\n### Batch Loading Optimization\n\nEnsure your loader correctly implements batch loading:\n\n```python\n# Correct: batch load with IN query\nasync def user_batch_loader(user_ids: list[int]):\n    async with get_session() as session:\n        result = await session.execute(\n            select(User).where(User.id.in_(user_ids))\n        )\n        users = result.scalars().all()\n        return build_list(users, user_ids, lambda u: u.id)\n```\n\n**Advanced: Optimize Query Fields with `_query_meta`**\n\nDataLoader can access required field information via `self._query_meta` to query only necessary data:\n\n```python\nfrom aiodataloader import DataLoader\n\nclass UserLoader(DataLoader):\n    async def batch_load_fn(self, user_ids: list[int]):\n        # Get fields required by response model\n        required_fields = self._query_meta.get('fields', ['*'])\n\n        # Query only required fields (optimize SQL query)\n        async with get_session() as session:\n            # If fields specified, query only those fields\n            if required_fields != ['*']:\n                columns = [getattr(User, f) for f in required_fields]\n                result = await session.execute(\n                    select(*columns).where(User.id.in_(user_ids))\n                )\n            else:\n                result = await session.execute(\n                    select(User).where(User.id.in_(user_ids))\n                )\n\n            users = result.scalars().all()\n            return build_list(users, user_ids, lambda u: u.id)\n```\n\n**Advantages**:\n- If `UserResponse` only needs `id` and `name`, SQL queries only these two fields\n- Reduce data transfer and memory usage\n- Improve query performance, especially for tables with many fields\n\n**Note**: `self._query_meta` is populated after Resolver's first scan.\n\n## Real-World Example\n\n### Scenario: Project Management System\n\nRequirements: Fetch all Sprints for a team, including:\n- All Stories for each Sprint\n- All Tasks for each Story\n- Owner for each Task\n- Statistics for each layer (total tasks, total estimates, etc.)\n\n```python\nfrom pydantic import BaseModel, ConfigDict\nfrom typing import Optional, List\nfrom pydantic_resolve import (\n    Resolver, Loader, LoadBy,\n    ExposeAs, Collector, SendTo,\n    base_entity, Relationship, config_global_resolver,\n    build_list, DefineSubset, SubsetConfig\n)\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy import select\n\n# 0. Define data loaders\nasync def user_batch_loader(user_ids: list[int]):\n    \"\"\"Batch load users\"\"\"\n    async with get_db_session() as session:\n        result = await session.execute(select(User).where(User.id.in_(user_ids)))\n        users = result.scalars().all()\n        return build_list(users, user_ids, lambda u: u.id)\n\nasync def story_to_tasks_loader(story_ids: list[int]):\n    \"\"\"Batch load Tasks for Stories\"\"\"\n    async with get_db_session() as session:\n        result = await session.execute(select(Task).where(Task.story_id.in_(story_ids)))\n        tasks = result.scalars().all()\n        return build_list(tasks, story_ids, lambda t: t.story_id)\n\nasync def sprint_to_stories_loader(sprint_ids: list[int]):\n    \"\"\"Batch load Stories for Sprints\"\"\"\n    async with get_db_session() as session:\n        result = await session.execute(select(Story).where(Story.sprint_id.in_(sprint_ids)))\n        stories = result.scalars().all()\n        return build_list(stories, sprint_ids, lambda s: s.sprint_id)\n\n# 1. Define entities and ERD\nBaseEntity = base_entity()\n\nclass UserEntity(BaseModel):\n    \"\"\"User entity\"\"\"\n    id: int\n    name: str\n    email: str\n\nclass TaskEntity(BaseModel, BaseEntity):\n    \"\"\"Task entity\"\"\"\n    __relationships__ = [\n        Relationship(field='owner_id', target_kls=UserEntity, loader=user_batch_loader)\n    ]\n    id: int\n    name: str\n    owner_id: int\n    story_id: int\n    estimate: int\n\nclass StoryEntity(BaseModel, BaseEntity):\n    \"\"\"Story entity\"\"\"\n    __relationships__ = [\n        Relationship(field='id', target_kls=list[TaskEntity], loader=story_to_tasks_loader),\n        Relationship(field='owner_id', target_kls=UserEntity, loader=user_batch_loader)\n    ]\n    id: int\n    name: str\n    owner_id: int\n    sprint_id: int\n\nclass SprintEntity(BaseModel, BaseEntity):\n    \"\"\"Sprint entity\"\"\"\n    __relationships__ = [\n        Relationship(field='id', target_kls=list[StoryEntity], loader=sprint_to_stories_loader)\n    ]\n    id: int\n    name: str\n    team_id: int\n\n# Register ERD\nconfig_global_resolver(BaseEntity.get_diagram())\n\n# 2. Define response models (use DefineSubset to select fields from entities)\n\n# Base user response\nclass UserResponse(DefineSubset):\n    __subset__ = (UserEntity, ('id', 'name'))\n\n# Scenario 1: Basic data composition - Use LoadBy to auto-resolve related data\nclass TaskResponse(DefineSubset):\n    __subset__ = SubsetConfig(\n        kls=TaskEntity,\n        fields=['id', 'name', 'estimate', 'owner_id']\n    )\n\n    # LoadBy auto-resolves owner based on Relationship definition in ERD\n    owner: Annotated[Optional[UserResponse], LoadBy('owner_id')] = None\n\n# Scenario 2: Parent exposes data to child nodes - Task names need Story prefix\nclass TaskResponseWithPrefix(DefineSubset):\n    __subset__ = SubsetConfig(\n        kls=TaskEntity,\n        fields=['id', 'name', 'estimate', 'owner_id']\n    )\n\n    owner: Annotated[Optional[UserResponse], LoadBy('owner_id')] = None\n\n    # post method can access data exposed by ancestor nodes\n    full_name: str = \"\"\n    def post_full_name(self, ancestor_context):\n        # Get story_name exposed by parent (Story)\n        story_name = ancestor_context.get('story_name')\n        return f\"{story_name} - {self.name}\"\n\n# Scenario 3: Compute extra fields - Story needs to calculate total estimate of all Tasks\nclass StoryResponse(DefineSubset):\n    __subset__ = SubsetConfig(\n        kls=StoryEntity,\n        fields=['id', 'name', 'owner_id'],\n        expose_as=[('name', 'story_name')]  # Expose to child nodes (used by Scenario 2)\n    )\n\n    # LoadBy auto-resolves tasks based on Relationship definition in ERD\n    tasks: Annotated[List[TaskResponse], LoadBy('id')] = []\n\n    # post_ method executes after all resolve_ methods complete\n    total_estimate: int = 0\n    def post_total_estimate(self):\n        return sum(t.estimate for t in self.tasks)\n\n# Scenario 4: Parent collects data from child nodes - Story needs to collect all involved developers\nclass TaskResponseForCollect(DefineSubset):\n    __subset__ = SubsetConfig(\n        kls=TaskEntity,\n        fields=['id', 'name', 'estimate', 'owner_id'],\n    )\n\n    owner: Annotated[Optional[UserResponse], LoadBy('owner_id'), SendTo('related_users')] = None\n\nclass StoryResponseWithCollect(DefineSubset):\n    __subset__ = (StoryEntity, ('id', 'name', 'owner_id'))\n\n    tasks: Annotated[List[TaskResponseForCollect], LoadBy('id')] = []\n\n    # Collect all child node owners\n    related_users: List[UserResponse] = []\n    def post_related_users(self, collector=Collector(alias='related_users')):\n        return collector.values()\n\n# Sprint response model - Combines all above features\nclass SprintResponse(DefineSubset):\n    __subset__ = (SprintEntity, ('id', 'name'))\n\n    # Use LoadBy to auto-resolve stories\n    stories: Annotated[List[StoryResponse], LoadBy('id')] = []\n\n    # Calculate statistics (total estimate of all stories)\n    total_estimate: int = 0\n    def post_total_estimate(self):\n        return sum(s.total_estimate for s in self.stories)\n\n# 3. API endpoint\n@app.get(\"/sprints\", response_model=List[SprintResponse])\nasync def get_sprints(session: AsyncSession = Depends(get_session)):\n    \"\"\"Fetch all Sprints with complete hierarchical data\"\"\"\n    sprints_data = await get_sprints_from_db(session)\n    await session.close()\n\n    sprints = [SprintResponse.model_validate(s) for s in sprints_data]\n    result = await Resolver().resolve(sprints)\n\n    return result\n```\n\n**Architectural Advantages**:\n- **Entity-Response Separation**: Entities define business entities and relationships, Responses define API return structures\n- **Reusable Relationship Definitions**: Define relationships once via ERD, all response models can use `LoadBy` for auto-resolution\n- **Type Safety**: DefineSubset ensures field types are inherited from entities\n- **Flexible Composition**: Define different response models based on the same entities and reuse DataLoader\n- **Query Optimization**: DataLoader can access required field info via `self._query_meta` to query only necessary data (e.g., SQL `SELECT` only required columns)\n\n**Scenario Coverage**:\n- **Scenario 1**: Basic data composition - Auto-resolve related data\n- **Scenario 2**: Expose - Parent nodes expose data to child nodes (e.g., Task uses Story's name)\n- **Scenario 3**: post - Compute extra fields (e.g., calculate total estimates)\n- **Scenario 4**: Collect - Parent nodes collect data from child nodes (e.g., collect all developers)\n\nEach scenario is independent and reusable, can be combined as needed.\n\n## Visualizing Dependencies with fastapi-voyager\n\n**pydantic-resolve** works best with [fastapi-voyager](https://github.com/allmonday/fastapi-voyager) - a powerful visualization tool that makes complex data relationships easy to understand.\n\n### Why fastapi-voyager?\n\u003cimg width=\"1564\" height=\"770\" alt=\"image\" src=\"https://github.com/user-attachments/assets/12d9e664-8ae0-4f8f-a99a-c533245e75cb\" /\u003e\n\n\u003cimg width=\"1463\" height=\"521\" alt=\"image\" src=\"https://github.com/user-attachments/assets/739c7ae7-3fbf-4a92-afca-39ab61fe87f5\" /\u003e\n\n\npydantic-resolve's declarative approach hides execution details, which can make it hard to understand **what's happening under the hood**. fastapi-voyager solves this by:\n\n- **Color-coded operations**: See `resolve`, `post`, `expose`, and `collect` at a glance\n- **Interactive exploration**: Click nodes to highlight upstream/downstream dependencies\n- **ERD visualization**: View entity relationships defined in your data models\n- **Source code navigation**: Double-click any node to jump to its definition\n- **Quick search**: Find models and trace their relationships instantly\n\n### Installation\n\n```bash\npip install fastapi-voyager\n```\n\n### Basic Setup\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi_voyager import create_voyager\n\napp = FastAPI()\n\n# Mount voyager to visualize your API\napp.mount('/voyager', create_voyager(\n    app,\n    enable_pydantic_resolve_meta=True  # Show pydantic-resolve metadata\n))\n```\n\nVisit `http://localhost:8000/voyager` to see the interactive visualization!\n\n### Understanding the Visualization\n\nWhen you enable `enable_pydantic_resolve_meta=True`, fastapi-voyager uses color-coded markers to show pydantic-resolve operations:\n\n#### Field Markers\n\n- **● resolve** - Field data is loaded via `resolve_{field}` method or `LoadBy`\n- **● post** - Field is computed via `post_{field}` method after all resolves complete\n- **● expose as** - Field is exposed to descendant nodes via `ExposeAs`\n- **● send to** - Field data is sent to parent collectors via `SendTo`\n- **● collectors** - Field collects data from child nodes via `Collector`\n\n#### Example\n\n```python\nclass TaskResponse(BaseModel):\n    id: int\n    name: str\n    owner_id: int\n\n    # resolve: loaded via DataLoader\n    owner: Annotated[Optional[UserResponse], LoadBy('owner_id')] = None\n\n    # send to: owner data sent to parent's collector\n    owner: Annotated[Optional[UserResponse], LoadBy('owner_id'), SendTo('related_users')] = None\n\nclass StoryResponse(BaseModel):\n    id: int\n\n    # expose as: name exposed to descendants\n    name: Annotated[str, ExposeAs('story_name')]\n\n    # resolve: tasks loaded via DataLoader\n    tasks: Annotated[List[TaskResponse], LoadBy('id')] = []\n\n    # post: computed from tasks\n    total_estimate: int = 0\n    def post_total_estimate(self):\n        return sum(t.estimate for t in self.tasks)\n\n    # collectors: collects from child nodes\n    related_users: List[UserResponse] = []\n    def post_related_users(self, collector=Collector(alias='related_users')):\n        return collector.values()\n```\n\n**In fastapi-voyager**, you'll see:\n- `owner` field marked with resolve and send to\n- `name` field marked with expose as: story_name\n- `tasks` field marked with resolve\n- `total_estimate` field marked with post\n- `related_users` field marked with collectors: related_users\n\n### Visualizing Entity Relationships (ERD)\n\nIf you're using ERD to define entity relationships, fastapi-voyager can visualize them:\n\n```python\nfrom pydantic_resolve import base_entity, Relationship, config_global_resolver\n\n# Define entities with relationships\nBaseEntity = base_entity()\n\nclass TaskEntity(BaseModel, BaseEntity):\n    __relationships__ = [\n        Relationship(field='owner_id', target_kls=UserEntity, loader=user_batch_loader)\n    ]\n    id: int\n    name: str\n    owner_id: int\n\nclass StoryEntity(BaseModel, BaseEntity):\n    __relationships__ = [\n        Relationship(field='id', target_kls=list[TaskEntity], loader=story_to_tasks_loader)\n    ]\n    id: int\n    name: str\n\n# Register ERD\ndiagram = BaseEntity.get_diagram()\nconfig_global_resolver(diagram)\n\n# Visualize it in voyager\napp.mount('/voyager', create_voyager(\n    app,\n    er_diagram=diagram,  # Show entity relationships\n    enable_pydantic_resolve_meta=True\n))\n```\n\n### Interactive Features\n\n#### Click to Highlight\nClick any model or route to see:\n- **Upstream**: What this model depends on\n- **Downstream**: What depends on this model\n\n#### Double-Click to View Code\nDouble-click any node to:\n- View the source code (if configured)\n- Open the file in VSCode (by default)\n\n#### Quick Search\n- Press `Shift + Click` on a node to search for it\n- Use the search box to find models by name\n- See related models highlighted automatically\n\n### Pro Tips\n\n1. **Start Simple**: Begin with `enable_pydantic_resolve_meta=False` to see the basic structure\n2. **Enable Metadata**: Turn on `enable_pydantic_resolve_meta=True` to see data flow\n3. **Use ERD View**: Toggle ERD view to understand entity-level relationships\n4. **Trace Data Flow**: Click a node and follow the colored links to understand data dependencies\n\n### Live Demo\n\nCheck out the [live demo](https://www.newsyeah.fun/voyager/?tag=sample_1) to see fastapi-voyager in action!\n\n### Learn More\n\n- [fastapi-voyager Documentation](https://github.com/allmonday/fastapi-voyager)\n- [Example Project](https://github.com/allmonday/composition-oriented-development-pattern)\n\n---\n\n**Key Insight**: fastapi-voyager turns pydantic-resolve's \"hidden magic\" into **visible, understandable data flows**, making it much easier to debug, optimize, and explain your code to others!\n\n## Why Not GraphQL?\n\nAlthough pydantic-resolve is inspired by GraphQL, it's better suited as a BFF (Backend For Frontend) layer solution:\n\n| Feature | GraphQL | pydantic-resolve |\n|----------|---------|------------------|\n| Performance | Requires complex DataLoader configuration | Built-in batch loading |\n| Type Safety | Requires additional toolchain | Native Pydantic type support |\n| Learning Curve | Steep (Schema, Resolver, Loader...) | Gentle (only need Pydantic) |\n| Debugging | Difficult | Simple (standard Python code) |\n| Integration | Requires additional server | Seamless integration with existing frameworks |\n| Flexibility | Queries too flexible, hard to optimize | Explicit API contracts |\n\n## More Resources\n\n- **Full Documentation**: https://allmonday.github.io/pydantic-resolve/\n- **Example Project**: https://github.com/allmonday/composition-oriented-development-pattern\n- **Live Demo**: https://www.newsyeah.fun/voyager/?tag=sample_1\n- **API Reference**: https://allmonday.github.io/pydantic-resolve/api/\n\n## Development\n\n```bash\n# Clone repository\ngit clone https://github.com/allmonday/pydantic_resolve.git\ncd pydantic_resolve\n\n# Install development dependencies\nuv venv\nsource .venv/bin/activate\nuv pip install -e \".[dev]\"\n\n# Run tests\nuv run pytest tests/\n\n# View test coverage\ntox -e coverage\n```\n\n## License\n\nMIT License\n\n## Author\n\ntangkikodo (allmonday@126.com)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fallmonday%2Fpydantic-resolve","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fallmonday%2Fpydantic-resolve","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fallmonday%2Fpydantic-resolve/lists"}