Skip to content

Instantly share code, notes, and snippets.

@kazqvaizer
Created November 17, 2025 02:01
Show Gist options
  • Select an option

  • Save kazqvaizer/c5edaaf27521993da4727a0519631168 to your computer and use it in GitHub Desktop.

Select an option

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 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