Created
November 17, 2025 02:01
-
-
Save kazqvaizer/c5edaaf27521993da4727a0519631168 to your computer and use it in GitHub Desktop.
Pydantic-driven schema layer to replace DRF serializers and OpenAPI models, using resolvable Django model references
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This module introduces a Pydantic-first approach for handling Django model | |
| # references in API schemas, serving as a lightweight alternative to DRF | |
| # serializers and their OpenAPI integrations. Instead of relying on DRF’s | |
| # serialization layer, it defines a resolvable wrapper that represents Django | |
| # model instances by ID while keeping schema definitions fully Pydantic-based. | |
| # A dynamic OrmResolvable type generates model-specific proxies, and a recursive | |
| # resolver hydrates these proxies into actual Django instances after validation. | |
| # The ProjectBaseModel hooks this resolution step into Pydantic’s lifecycle, | |
| # enabling clean, declarative API schemas that remain tightly aligned with | |
| # Django models without DRF’s overhead. This is intended for projects wishing | |
| # to maintain strict Pydantic schemas while still integrating with Django ORM. | |
| # Caution: that was not tested properly yet (that`is a halfway AI slop), | |
| # so wait for updates or place your opinions :( | |
| import uuid | |
| from typing import Any, TypeVar, Type | |
| from django.db import models | |
| from pydantic import BaseModel, PrivateAttr, model_validator | |
| from typing_extensions import Generic | |
| from users.models import User | |
| T = TypeVar("T", bound=models.Model) | |
| class OrmResolvableBase(BaseModel, Generic[T]): | |
| id: uuid.UUID | |
| _resolved: Any = PrivateAttr(default=None) | |
| _model: Type[models.Model] | |
| model_config = {"arbitrary_types_allowed": True} | |
| @property | |
| def instance(self) -> models.Model: | |
| if self._resolved is None: | |
| raise ValueError("OrmResolvable object was not resolved yet") | |
| return self._resolved | |
| class OrmResolvable(Generic[T]): | |
| def __class_getitem__(cls, model: Type[T]) -> Type[OrmResolvableBase]: | |
| name = f"Resolvable_{model.__name__}" | |
| namespace = {"_model": PrivateAttr(default=model)} | |
| return type(name, (OrmResolvableBase,), namespace) | |
| def resolve_all(obj: Any) -> Any: | |
| if isinstance(obj, OrmResolvableBase): | |
| if obj._resolved is None: | |
| model = obj._model | |
| try: | |
| instance = model.objects.get(pk=obj.id) | |
| except model.DoesNotExist: | |
| raise ValueError(f"{model.__name__} with id={obj.id} not found") | |
| obj._resolved = instance | |
| return obj | |
| if isinstance(obj, (list, tuple)): | |
| for idx, val in enumerate(obj): | |
| obj[idx] = resolve_all(val) | |
| return obj | |
| if isinstance(obj, BaseModel): | |
| for name, val in obj.__dict__.items(): | |
| setattr(obj, name, resolve_all(val)) | |
| return obj | |
| return obj | |
| # How to use that crap: | |
| class ProjectBaseModel(BaseModel): | |
| model_config = {"arbitrary_types_allowed": True} | |
| @model_validator(mode="after") | |
| def auto_resolve(self): | |
| resolve_all(self) | |
| return self | |
| class SomeDataSchema(ProjectBaseModel): | |
| title: str | |
| body: str | |
| author: OrmResolvable[User] | |
| SomeDataSchema(**{"title": "asd", "body": "a123", "author": {"id": "3b3f2256-d08b-46f3-9351-45286ef8451b"}}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment