Skip to main content

pydantic-resolve turns pydantic from a static data container into a powerful composable component.

Project description

Pydantic Resolve

Declarative data assembly for Pydantic — eliminate N+1 queries with minimal code.

pypi PyPI Downloads Python Versions CI

中文版


pydantic-resolve is inspired by GraphQL. It builds database-independent application-layer Entity Relationship Diagrams using DataLoader, providing rich data assembly and post-processing capabilities. It can also auto-generate GraphQL queries and MCP services.

Why pydantic-resolve?

Core capabilities:

Feature What it does
Automatic Batching DataLoader eliminates N+1 queries automatically
Declarative Assembly Declare dependencies, framework handles the rest
Entity-First Architecture ER Diagram defines relationships, LoadBy auto-resolves
GraphQL Support Generate schema from ERD, query with dynamic models
MCP Integration Expose GraphQL APIs to AI agents with progressive disclosure

One line to fetch nested data:

class Task(BaseModel):
    owner_id: int
    owner: Optional[User] = None

    def resolve_owner(self, loader=Loader(user_loader)):
        return loader.load(self.owner_id)  # That's it!

# Resolver automatically batches all owner lookups into one query
result = await Resolver().resolve(tasks)

Quick Start

Install

pip install pydantic-resolve

The N+1 Problem

# Traditional: 1 + N queries
for task in tasks:
    task.owner = await get_user(task.owner_id)  # N queries!

The pydantic-resolve Solution

from pydantic import BaseModel
from typing import Optional, List
from pydantic_resolve import Resolver, Loader, build_list

# 1. Define your loaders (batch queries)
async def user_loader(ids: list[int]):
    users = await db.query(User).filter(User.id.in_(ids)).all()
    return build_list(users, ids, lambda u: u.id)

async def task_loader(sprint_ids: list[int]):
    tasks = await db.query(Task).filter(Task.sprint_id.in_(sprint_ids)).all()
    return build_list(tasks, sprint_ids, lambda t: t.sprint_id)

# 2. Define your schema with resolve methods
class TaskResponse(BaseModel):
    id: int
    name: str
    owner_id: int

    owner: Optional[dict] = None
    def resolve_owner(self, loader=Loader(user_loader)):
        return loader.load(self.owner_id)

class SprintResponse(BaseModel):
    id: int
    name: str

    tasks: List[TaskResponse] = []
    def resolve_tasks(self, loader=Loader(task_loader)):
        return loader.load(self.id)

# 3. Resolve - framework handles batching automatically
@app.get("/sprints")
async def get_sprints():
    sprints = await get_sprint_data()
    return await Resolver().resolve([SprintResponse.model_validate(s) for s in sprints])

Result: 1 query per loader, regardless of data depth.


Core Concepts

Resolve: Declarative Data Loading

Instead of imperative data fetching, declare what you need:

class Task(BaseModel):
    owner_id: int
    owner: Optional[User] = None

    def resolve_owner(self, loader=Loader(user_loader)):
        return loader.load(self.owner_id)

The framework:

  1. Collects all owner_id values
  2. Batches them into one query
  3. Maps results back to correct objects

DataLoader: Automatic Batching

DataLoader batches multiple requests within the same event loop tick:

# Without DataLoader: 100 tasks = 100 user queries
# With DataLoader: 100 tasks = 1 user query (WHERE id IN (...))

async def user_loader(user_ids: list[int]):
    return await db.query(User).filter(User.id.in_(user_ids)).all()

Expose & Collect: Cross-layer Data Flow

In nested data structures, parent and child nodes often need to share data. Traditional approaches require explicit parameter passing or tight coupling. pydantic-resolve provides two declarative mechanisms:

  • ExposeAs: Parent nodes expose data to all descendants (downward flow)
  • SendTo + Collector: Child nodes send data to parent collectors (upward flow)

This creates a clean separation — parent doesn't need to know child's structure, and child doesn't need explicit parent references.

from pydantic_resolve import ExposeAs, Collector, SendTo
from typing import Annotated

# 1. Parent EXPOSES data to descendants (downward flow)
class Story(BaseModel):
    name: Annotated[str, ExposeAs('story_name')]
    tasks: List[Task] = []

# 2. Child ACCESSES ancestor context (no explicit parent reference needed)
class Task(BaseModel):
    def post_full_path(self, ancestor_context):
        return f"{ancestor_context['story_name']} / {self.name}"

# 3. Child SENDS data to parent collector (upward flow)
class Task(BaseModel):
    owner: Annotated[User, SendTo('contributors')] = None

class Story(BaseModel):
    contributors: List[User] = []
    def post_contributors(self, collector=Collector('contributors')):
        return collector.values()  # Auto-deduplicated list of all task owners

Use cases:

  • Pass configuration/context down to nested objects (e.g., user permissions, locale)
  • Aggregate results up from nested objects (e.g., collect all unique tags from posts)

Advanced Features

Entity-First Architecture

Define business entities independent of database schema.

Why Entity-First vs DB-based relationships?

Aspect DB-based (ORM) Entity-First (pydantic-resolve)
Flexibility Tied to database schema Define relationships at application layer
Data Sources Single database Cross multiple sources (PostgreSQL, MongoDB, Redis, RPC)
Encapsulation Exposes FK fields (owner_id) Loader implementation hidden from API
API Contract Changes when DB changes Stable, decoupled from storage
from pydantic_resolve import base_entity, Relationship, LoadBy

BaseEntity = base_entity()

# Entity defines business relationship, not database FK
class TaskEntity(BaseModel, BaseEntity):
    __relationships__ = [
        # Loader can query Postgres, call RPC, or fetch from Redis
        # API consumers don't need to know where data comes from
        Relationship(field='owner_id', target_kls=UserEntity, loader=user_loader)
    ]
    id: int
    name: str
    description: Optional[str] = None
    status: str  # todo, in_progress, done
    owner_id: int  # Internal FK, can be hidden from API

# Response schema: choose what to expose
class TaskResponse(DefineSubset):
    __subset__ = (TaskEntity, ('id', 'name'))  # owner_id excluded
    owner: Annotated[User, LoadBy('owner_id')] = None  # Auto-resolved!

Key benefits:

  • Change loader implementation (SQL → RPC) without touching Response code
  • Mix data from multiple sources in single entity graph
  • Hide internal IDs from API, expose only business concepts

→ Full Entity-First Guide

GraphQL Support

Generate GraphQL schema from ERD:

from pydantic_resolve.graphql import GraphQLHandler

handler = GraphQLHandler(BaseEntity.get_diagram())
result = await handler.execute("{ users { id name posts { title } } }")

→ GraphQL Documentation

MCP Integration

Expose GraphQL APIs to AI agents with progressive disclosure:

from pydantic_resolve.graphql.mcp import create_mcp_server

mcp = create_mcp_server(apps=[AppConfig(name="blog", er_diagram=diagram)])
mcp.run()  # AI agents can now discover and query your API

→ MCP Documentation

Visualization

Interactive schema exploration with fastapi-voyager:

from fastapi_voyager import create_voyager

app.mount('/voyager', create_voyager(app, er_diagram=BaseEntity.get_diagram()))

pydantic-resolve vs GraphQL

Feature GraphQL pydantic-resolve
N+1 Prevention Manual DataLoader setup Built-in automatic batching
Type Safety Separate schema files Native Pydantic types
Learning Curve Steep (Schema, Resolvers, Loaders) Gentle (just Pydantic)
Debugging Complex introspection Standard Python debugging
Integration Requires dedicated server Works with any framework
Query Flexibility Any client can query anything Explicit API contracts

Resources


License

MIT License

Author

tangkikodo (allmonday@126.com)

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_resolve-3.3.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_resolve-3.3.0-py3-none-any.whl (106.4 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_resolve-3.3.0.tar.gz.

File metadata

  • Download URL: pydantic_resolve-3.3.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pydantic_resolve-3.3.0.tar.gz
Algorithm Hash digest
SHA256 b848f00d404d6c47d8b342a7a17c7dbf498bc436c5d89911506b13a70da0ce3e
MD5 dffb835d198baddc7e1f37a51cb5945d
BLAKE2b-256 5f437d7660becc675000bb08ae51ef6fd0274cc192d7cc8a991978ce6203a928

See more details on using hashes here.

File details

Details for the file pydantic_resolve-3.3.0-py3-none-any.whl.

File metadata

  • Download URL: pydantic_resolve-3.3.0-py3-none-any.whl
  • Upload date:
  • Size: 106.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pydantic_resolve-3.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 48f585548874032cb421d62b1d04ad777921ae73220b71ea6d592cda1107d952
MD5 9f2a22c036dd1b6a2ceccad1f5d5e6f9
BLAKE2b-256 34e6221a0d17a880bfc6387e73052bc4f84890e2ed1f29e9396862faf7415af2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page