{"id":50595791,"url":"https://github.com/KLR-Pattern/pydantic-resolve","last_synced_at":"2026-06-18T05:00:57.028Z","repository":{"id":121567728,"uuid":"610649159","full_name":"KLR-Pattern/pydantic-resolve","owner":"KLR-Pattern","description":"pydantic-resolve is a Pythonic clean architecture implementation","archived":false,"fork":false,"pushed_at":"2026-06-17T23:59:17.000Z","size":5742,"stargazers_count":325,"open_issues_count":5,"forks_count":13,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-18T00:21:00.131Z","etag":null,"topics":["bff","fastapi","fullstack","graphql","mcp","pydantic","python"],"latest_commit_sha":null,"homepage":"https://klr-pattern.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/KLR-Pattern.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-06-10T01:57:30.000Z","dependencies_parsed_at":"2023-09-29T06:47:16.976Z","dependency_job_id":"c3d62574-5777-4c0f-a7f4-30ba5ec91578","html_url":"https://github.com/KLR-Pattern/pydantic-resolve","commit_stats":null,"previous_names":["allmonday/pydantic_resolve","klr-pattern/pydantic-resolve"],"tags_count":65,"template":false,"template_full_name":null,"purl":"pkg:github/KLR-Pattern/pydantic-resolve","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KLR-Pattern%2Fpydantic-resolve","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KLR-Pattern%2Fpydantic-resolve/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KLR-Pattern%2Fpydantic-resolve/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KLR-Pattern%2Fpydantic-resolve/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KLR-Pattern","download_url":"https://codeload.github.com/KLR-Pattern/pydantic-resolve/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KLR-Pattern%2Fpydantic-resolve/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34476728,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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","mcp","pydantic","python"],"created_at":"2026-06-05T14:00:44.488Z","updated_at":"2026-06-18T05:00:57.005Z","avatar_url":"https://github.com/KLR-Pattern.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# Pydantic Resolve\n\n\u003e Clean Architecture for Python — define business entities, declare relationships, let the framework assemble your data.\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/v/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**Requirements:** Python 3.10+, Pydantic v2\n\n---\n\n## The Missing Layer\n\nMost FastAPI projects follow the same pattern: define SQLAlchemy ORM models first, then create Pydantic schemas that mirror them. This \"ORM-First\" approach is so common that many developers have never questioned it. But as projects grow, it creates systemic problems:\n\n| # | Problem | Clean Architecture Violation |\n|---|---------|------------------------------|\n| 1 | Schema passively follows ORM — same fields defined twice | API contract (Frameworks) is tied to DB design (Adapters) |\n| 2 | Business concepts lost — frontend sees `owner_id` instead of \"task has an owner\" | Enterprise Business Rules are permeated by DB structure |\n| 3 | Data assembly has no home — join logic scattered across Repository / Service / Route | Application Business Rules layer is missing |\n| 4 | Multi-source data is hard — each new source means conversion code everywhere | No unified Interface Adapter abstraction |\n| 5 | Schema reuse is hard — copy-paste for UserSummary / UserDetail / UserAvatar | No Enterprise entity to derive Frameworks responses from |\n\nThese are not individual tooling issues. They all trace back to one architectural violation: **the system has no Enterprise Business Rules layer independent of the database**. In Clean Architecture terms, the Frameworks layer (ORM) has colonized the Enterprise layer.\n\n```python\n# The data assembly dilemma: where does this logic go?\n@router.get(\"/tasks\")\nasync def get_tasks():\n    tasks = await task_service.get_tasks()\n\n    # Collect IDs, batch query, build mapping, assemble result...\n    user_ids = list({t.owner_id for t in tasks})\n    users = await user_service.get_users_by_ids(user_ids)\n    user_map = {u.id: u for u in users}\n\n    result = []\n    for task in tasks:\n        task_dict = task.model_dump()\n        task_dict['owner'] = user_map.get(task.owner_id)\n        result.append(TaskResponse(**task_dict))\n    return result\n```\n\nWhether this code lives in Repository, Service, or Route, the problem is the same: data assembly logic has no proper place in the traditional three-layer architecture.\n\n## Clean Architecture Layer Map\n\n**pydantic-resolve** provides the missing layer. Its components map 1:1 to Clean Architecture:\n\n| Clean Architecture Layer | pydantic-resolve Component |\n|--------------------------|---------------------------|\n| Enterprise Business Rules | Entity + ER Diagram |\n| Application Business Rules | Resolver + resolve/post |\n| Interface Adapters | Loader (data access) |\n| Frameworks \u0026 Interfaces | FastAPI routes + GraphQL + MCP |\n\n```mermaid\nflowchart LR\n    subgraph FW[\"Frameworks \u0026 Interfaces\"]\n        R[\"Response\u003cbr/\u003eFastAPI routes\"]\n    end\n    subgraph APP[\"Application Business Rules\"]\n        RV[\"Resolver\u003cbr/\u003eresolve / post\"]\n    end\n    subgraph ENT[\"Enterprise Business Rules\"]\n        E[\"Entity + ER Diagram\"]\n    end\n    subgraph ADP[\"Interface Adapters\"]\n        L[\"Loader\"]\n    end\n    FW --\u003e APP --\u003e ENT --\u003e ADP\n```\n\nThe dependency direction always points inward: Entity doesn't know about Loader. Loader doesn't know about FastAPI. FastAPI doesn't know about the database.\n\nFor the full architectural analysis, see [Clean Architecture for Python](./docs/architecture_entity_first.md).\n\n---\n\n## How pydantic-resolve Works\n\n**pydantic-resolve** provides three mechanisms — one per Clean Architecture layer:\n\n| What you need | What you write | Clean Architecture Layer | What the framework does |\n|------|----------------|--------------------------|-------------------------|\n| Load related data | `resolve_*` + `Loader(...)` | Interface Adapters | Batch lookups and map results back |\n| Compute derived fields | `post_*` | Application Business Rules | Run after descendants are fully resolved |\n| Reuse relationship declarations | ER Diagram + `AutoLoad` | Enterprise Business Rules | Centralize relationship wiring for many models |\n\nThe same ERD also powers GraphQL queries, MCP services, and admin tools:\n\n```mermaid\nflowchart TB\n    entity[\"Entity + ERD\u003cbr/\u003eEnterprise Business Rules\"]\n    resolve[\"Resolver\u003cbr/\u003eApplication Business Rules\"]\n    graphql[\"GraphQL Generator\"]\n    usecase[\"UseCase Service\"]\n    api[\"REST API\"]\n    mcp1[\"MCP Service\"]\n    mcp2[\"MCP Service\"]\n\n    entity --\u003e resolve\n    entity --\u003e graphql\n    resolve --\u003e usecase\n    usecase --\u003e api\n    usecase --\u003e mcp1\n    graphql --\u003e mcp2\n```\n\n### Before and After\n\n```python\n# Before: manual N+1 assembly in your route (Frameworks layer knows about DB)\n@router.get(\"/tasks\")\nasync def get_tasks():\n    tasks = await task_service.get_tasks()\n    user_ids = list({t.owner_id for t in tasks})\n    users = await user_service.get_users_by_ids(user_ids)\n    user_map = {u.id: u for u in users}\n    return [\n        TaskResponse(**{**t.model_dump(), 'owner': user_map.get(t.owner_id)})\n        for t in tasks\n    ]\n```\n\n```python\n# After: declare what's missing, let the framework assemble (layers stay clean)\nclass TaskView(BaseModel):\n    id: int\n    title: str\n    owner_id: int\n    owner: Optional[UserView] = None\n\n    def resolve_owner(self, loader=Loader(user_loader)):  # Interface Adapter\n        return loader.load(self.owner_id)\n\n@router.get(\"/tasks\")\nasync def get_tasks():\n    tasks = [TaskView.model_validate(t) for t in await task_repo.get_tasks()]\n    return await Resolver().resolve(tasks)  # Application Business Rules\n```\n\nAdvantages of the new approach:\n\n- **Separation of concerns**: Data loading logic moves from routes into models; routes only handle orchestration\n- **Declarative assembly**: `resolve_*` declares \"what data is needed\", the framework handles \"how to batch-fetch it\"\n- **Readability and maintainability**: Field definitions and their data sources live in the same class, clear at a glance\n\n## Quick Start\n\n### Install\n\n```bash\npip install pydantic-resolve\npip install pydantic-resolve[mcp]  # with MCP support\n```\n\n### The Example\n\nThroughout the Quick Start, we build one API:\n\n- `Sprint` has many `Task`\n- `Task` has one `owner` (a `User`)\n- The API also needs derived fields like `task_count` and `contributors`\n\nExpected response structure:\n\n```json\n{\n  \"id\": 1,\n  \"name\": \"Sprint 1\",\n  \"tasks\": [\n    {\n      \"id\": 101,\n      \"title\": \"Implement login\",\n      \"owner_id\": 1,\n      \"owner\": {\n        \"id\": 1,\n        \"name\": \"Alice\"\n      }\n    }\n  ],\n  \"task_count\": 1,\n  \"contributor_names\": [\"Alice\"]\n}\n```\n\nEach step adds one concept on top of the previous code.\n\n### Step 1: Load Related Data with `resolve_*` — *Interface Adapters*\n\nEvery response model has some fields already filled (from the database, from user input) and some fields that need to be fetched separately. `resolve_*` is how you declare those missing fields — it is your Interface Adapter.\n\n```python\nfrom typing import Optional\n\nfrom pydantic import BaseModel\nfrom pydantic_resolve import Loader, Resolver, build_object\n\n\nclass UserView(BaseModel):\n    id: int\n    name: str\n\n\nasync def user_loader(user_ids: list[int]):\n    users = await db.query(User).filter(User.id.in_(user_ids)).all()\n    return build_object(users, user_ids, lambda user: user.id)\n\n\nclass TaskView(BaseModel):\n    id: int\n    title: str\n    owner_id: int\n    owner: Optional[UserView] = None\n\n    def resolve_owner(self, loader=Loader(user_loader)):\n        return loader.load(self.owner_id)\n\n\ntasks = [TaskView.model_validate(task) for task in raw_tasks]\ntasks = await Resolver().resolve(tasks)\n```\n\nThat is the core idea of the library:\n\n- `owner` is missing data, so you describe how to fetch it.\n- `user_loader` receives all requested `owner_id` values together.\n- `Resolver().resolve(...)` walks the model tree and fills the field.\n\nA useful mental model is: **`resolve_*` means \"this field needs data from outside the current node.\"**\n\n### Step 2: Compose Nested Trees — *Application Business Rules*\n\nReal APIs rarely have just one relationship. When `Sprint` contains many `Task`s, and each `Task` already knows how to load its `owner`, the resolver walks the tree and batch-loads everything recursively.\n\n```python\nfrom typing import List\n\nfrom pydantic_resolve import build_list\n\n\nasync def task_loader(sprint_ids: list[int]):\n    tasks = await db.query(Task).filter(Task.sprint_id.in_(sprint_ids)).all()\n    return build_list(tasks, sprint_ids, lambda task: task.sprint_id)\n\n\nclass SprintView(BaseModel):\n    id: int\n    name: str\n    tasks: List[TaskView] = []\n\n    def resolve_tasks(self, loader=Loader(task_loader)):\n        return loader.load(self.id)\n\n\nsprints = [SprintView.model_validate(sprint) for sprint in raw_sprints]\nsprints = await Resolver().resolve(sprints)\n```\n\n**Result:** one query per loader, regardless of how many sprints or tasks you load.\n\nThis is why `resolve_*` is the best place to start. You can get value from the library before learning any advanced features.\n\n### Step 3: Compute Derived Fields with `post_*` — *Application Business Rules*\n\n`task_count` and `contributor_names` don't come from a query — they're derived from data already on the model. `post_*` handles these: it runs **after** all nested `resolve_*` calls have finished.\n\n```python\nclass SprintView(BaseModel):\n    id: int\n    name: str\n    tasks: List[TaskView] = []\n    task_count: int = 0\n    contributor_names: list[str] = []\n\n    def resolve_tasks(self, loader=Loader(task_loader)):\n        return loader.load(self.id)\n\n    def post_task_count(self):\n        return len(self.tasks)\n\n    def post_contributor_names(self):\n        return sorted({task.owner.name for task in self.tasks if task.owner})\n```\n\nExecution order:\n\n1. `resolve_tasks` loads the sprint's tasks.\n2. Each `TaskView.resolve_owner` loads its owner.\n3. `post_task_count` and `post_contributor_names` run after those nested fields are ready.\n\n| | `resolve_*` | `post_*` |\n|---|---|---|\n| Needs external IO? | Yes | Usually no |\n| Runs before descendants ready? | Yes | No |\n| Good for counts, sums, formatting? | Sometimes | Yes |\n| Return value resolved again? | Yes | No |\n\nThese two patterns cover most API endpoints. The next section covers cross-layer data flow — skip to [ER Diagram](#enterprise-business-rules-er-diagram--autoload) if you don't need it yet.\n\n### Step 4: Coordinate Parent and Child — *Cross-cutting Concern*\n\nWhen parent and child nodes need to share data without hard-coding references to each other:\n\n- `ExposeAs`: send ancestor data downward\n- `SendTo` + `Collector`: send child data upward\n\n```python\nfrom typing import Annotated\n\nfrom pydantic_resolve import Collector, ExposeAs, SendTo\n\n\nclass SprintView(BaseModel):\n    id: int\n    name: Annotated[str, ExposeAs('sprint_name')]\n    tasks: List[TaskView] = []\n    contributors: list[UserView] = []\n\n    def resolve_tasks(self, loader=Loader(task_loader)):\n        return loader.load(self.id)\n\n    def post_contributors(self, collector=Collector('contributors')):\n        return collector.values()\n\n\nclass TaskView(BaseModel):\n    id: int\n    title: str\n    owner_id: int\n    owner: Annotated[Optional[UserView], SendTo('contributors')] = None\n    full_title: str = \"\"\n\n    def resolve_owner(self, loader=Loader(user_loader)):\n        return loader.load(self.owner_id)\n\n    def post_full_title(self, ancestor_context):\n        return f\"{ancestor_context['sprint_name']} / {self.title}\"\n```\n\nUse this when the shape of the tree matters — for example, a child needs ancestor context (sprint name, permissions), or a parent needs to aggregate values from many descendants (all contributors, all tags).\n\n## Enterprise Business Rules: ER Diagram + AutoLoad\n\nER Diagram + `AutoLoad` is where Clean Architecture's Enterprise Business Rules layer fully materializes: relationships become the stable core, and every Response is just a different view of the same Entity graph.\n\nUp to this point, the Core API is enough. Stay there until relationship declarations start repeating across many response models.\n\nA common signal is when you see the same relation described again and again:\n\n- `TaskCard.resolve_owner`\n- `TaskDetail.resolve_owner`\n- `SprintBoard.resolve_tasks`\n- `SprintReport.resolve_tasks`\n\nAt that point, the problem is no longer \"how do I load this field?\" but \"where is the source of truth for relationships?\"\n\n### Cost vs Benefit\n\n| Question | Hand-written Core API | ER Diagram + `AutoLoad` |\n|----------|------------------------|--------------------------|\n| First endpoint | Faster | Slower |\n| Upfront setup | Low | Medium |\n| Reusing the same relation in many models | Repetitive | Centralized |\n| Changing a relationship later | Update many `resolve_*` methods | Update one ERD declaration |\n| GraphQL / MCP generation | Separate work | Natural extension |\n\nERD mode asks for more discipline up front:\n\n- Define entity classes.\n- Declare relationships explicitly.\n- Create `AutoLoad` from the same `diagram` used by the resolver.\n\nThat setup cost is real. The payoff is that relationship knowledge converges into one place — this is precisely the responsibility of Clean Architecture's Enterprise Business Rules layer (Entity Layer): defining core business knowledge independent of external frameworks, so that the database, API, GraphQL, and MCP are all just different projections of it.\n\n### The Same Example in ERD Mode\n\nHere is the same `Sprint -\u003e Task -\u003e User` example after moving relationship wiring into an ER Diagram:\n\n```python\nfrom typing import Annotated, Optional\n\nfrom pydantic import BaseModel\nfrom pydantic_resolve import Relationship, base_entity, config_global_resolver\n\n\nBaseEntity = base_entity()\n\n\nclass UserEntity(BaseModel, BaseEntity):\n    id: int\n    name: str\n\n\nclass TaskEntity(BaseModel, BaseEntity):\n    __relationships__ = [\n        Relationship(fk='owner_id', name='owner', target=UserEntity, loader=user_loader)\n    ]\n    id: int\n    title: str\n    owner_id: int\n\n\nclass SprintEntity(BaseModel, BaseEntity):\n    __relationships__ = [\n        Relationship(fk='id', name='tasks', target=list[TaskEntity], loader=task_loader)\n    ]\n    id: int\n    name: str\n\n\ndiagram = BaseEntity.get_diagram()\nAutoLoad = diagram.create_auto_load()\nconfig_global_resolver(diagram)\n\n\nclass TaskView(TaskEntity):\n    owner: Annotated[Optional[UserEntity], AutoLoad()] = None\n\n\nclass SprintView(SprintEntity):\n    tasks: Annotated[list[TaskView], AutoLoad()] = []\n    task_count: int = 0\n\n    def post_task_count(self):\n        return len(self.tasks)\n```\n\nCompared with the Core API version:\n\n- `resolve_owner` disappears.\n- `resolve_tasks` disappears.\n- The relationship definitions live in one place.\n- `post_*` still works exactly the same.\n\nIf you want to hide internal FK fields such as `owner_id`, add `DefineSubset` on top of the ERD setup:\n\n```python\nfrom pydantic_resolve import DefineSubset\n\n\nclass TaskSummary(DefineSubset):\n    __subset__ = (TaskEntity, ('id', 'title'))\n    owner: Annotated[Optional[UserEntity], AutoLoad()] = None\n```\n\n### If Your ORM Already Knows the Relationships\n\nOnce ERD mode makes sense conceptually, you can let the ORM describe the relationships for you and import them into the Enterprise layer:\n\n```python\nfrom pydantic_resolve import ErDiagram\nfrom pydantic_resolve.integration.mapping import Mapping\nfrom pydantic_resolve.integration.sqlalchemy import build_relationship\n\n\nentities = build_relationship(\n    mappings=[\n        Mapping(entity=SprintEntity, orm=SprintORM),\n        Mapping(entity=TaskEntity, orm=TaskORM),\n        Mapping(entity=UserEntity, orm=UserORM),\n    ],\n    session_factory=session_factory,\n)\n\ndiagram = ErDiagram(entities=[]).add_relationship(entities)\nAutoLoad = diagram.create_auto_load()\nconfig_global_resolver(diagram)\n```\n\n`build_relationship` supports **SQLAlchemy**, **Django**, and **Tortoise ORM**. This is a good later optimization when your ORM metadata is already stable and you want to avoid duplicating relationship declarations.\n\n## Adoption Path\n\n### 1. Interface Adapters First\n\nStart with `resolve_*` and `post_*` on one endpoint. You gain immediate N+1 protection without changing your architecture.\n\n### 2. Enterprise Business Rules When Ready\n\nWhen relationships start repeating across models, move them into ERD. This is the step where you establish your Enterprise layer.\n\n### 3. Let the Framework Absorb ORM Metadata\n\nWhen your ORM is stable, use `build_relationship()` to import existing relationship knowledge from the database layer.\n\n**ERD mode is a good fit when:**\n\n- The project has 3+ related entities reused across multiple response models.\n- The team wants one shared place to inspect and discuss relationships.\n- You want GraphQL or MCP generated from the same model graph.\n- You want to hide FK fields while keeping relationship definitions centralized.\n\n**Core API is usually enough when:**\n\n- You only have a few loading requirements.\n- You want each endpoint to stay maximally explicit.\n- The response shape is still changing quickly.\n\n[→ Full ERD-Driven Guide](https://allmonday.github.io/pydantic-resolve/erd_driven/)\n\n## Frameworks \u0026 Interfaces: Integrations\n\nERD not only drives REST APIs, but also powers GraphQL queries, MCP services, and admin tools.\n\n### GraphQL\n\nGenerate GraphQL schema from ERD and execute queries:\n\n```python\nfrom pydantic_resolve.graphql import GraphQLHandler\n\nhandler = GraphQLHandler(diagram)\nresult = await handler.execute(\"{ users { id name posts { title } } }\")\n# result.data == {\"users\": [{\"id\": 1, \"name\": \"Alice\", \"posts\": [{\"title\": \"Hello\"}]}, ...]}\n```\n\n[→ GraphQL Documentation](./demo/graphql/README.md)\n\n### MCP\n\nExpose GraphQL APIs to AI agents (requires `pip install pydantic-resolve[mcp]`):\n\n```python\nfrom pydantic_resolve import AppConfig, create_mcp_server\n\nmcp = create_mcp_server(apps=[AppConfig(name=\"blog\", er_diagram=diagram)])\nmcp.run()\n# Agents can then query: \"list all posts by user Alice\" → translated to GraphQL against your ERD\n```\n\n[→ MCP Documentation](https://allmonday.github.io/pydantic-resolve/api/)\n\n### Visualization\n\nInteractive ERD exploration with [fastapi-voyager](https://github.com/allmonday/fastapi-voyager):\n\n```python\nfrom fastapi_voyager import create_voyager\n\napp.mount('/voyager', create_voyager(app, er_diagram=diagram))\n```\n\n---\n\n## Comparisons\n\n### Entity-First (pydantic-resolve) vs ORM-First (traditional FastAPI)\n\n| Dimension | ORM-First | Entity-First |\n|-----------|-----------|-------------|\n| Type source of truth | ORM model | Entity (Pydantic) |\n| Relationship wiring | Repeated per endpoint | Centralized in ERD |\n| Data assembly | Manual in Service/Route | Automatic via Resolver |\n| N+1 prevention | Manual eager loading | Built-in DataLoader batching |\n| Multi-data source | Scattered conversion code | Unified Loader interface |\n| API contract stability | Tied to DB schema | Independent of DB |\n\n### pydantic-resolve vs GraphQL\n\n| Feature | GraphQL | pydantic-resolve |\n|---------|---------|------------------|\n| **N+1 Prevention** | Manual DataLoader setup | Built-in automatic batching |\n| **Type Safety** | Separate schema files | Native Pydantic types |\n| **Learning Curve** | Steep (Schema, Resolvers, Loaders) | Moderate (Loader/batch pattern required) |\n| **Debugging** | Complex introspection | Standard Python debugging |\n| **Integration** | Requires dedicated server | Works with any framework |\n| **Query Flexibility** | Any client can query anything | Explicit API contracts |\n\n\u003e **Note:** pydantic-resolve borrows the DataLoader batch pattern from GraphQL ecosystems. The main difference is that you keep your existing REST framework and get automatic batching without adopting a full GraphQL server. If your project already uses strawberry or ariadne and is happy with it, pydantic-resolve may be redundant.\n\n---\n\n## Resources\n\n- [Full Documentation](https://allmonday.github.io/pydantic-resolve/)\n- [Clean Architecture for Python (full paper)](./docs/architecture_entity_first.md)\n- [Example Project](https://github.com/allmonday/composition-oriented-development-pattern)\n- [Live Demo](https://www.fastapi-voyager.top/voyager/)\n- [Live Demo - GraphQL](https://www.fastapi-voyager.top/graphql)\n- [API Reference](https://allmonday.github.io/pydantic-resolve/api/)\n\n---\n\n## Credits\n\n- [Faster Breadth-First GraphQL Execution — Shopify Engineering](https://shopify.engineering/faster-breadth-first-graphql-execution)\n\n---\n\n## License\n\nMIT License\n\n## Author\n\ntangkikodo (allmonday@126.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKLR-Pattern%2Fpydantic-resolve","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FKLR-Pattern%2Fpydantic-resolve","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKLR-Pattern%2Fpydantic-resolve/lists"}