Dataclass to Pydantic BaseModel Conversion
Zach Bellay published on
3 min, 532 words
Note: This post is a dramaticized development experience I had at work that stemmed largely from miscommunication and bad assumptions. Nonetheless, if you're in a situation like this, here's what I did.
The Problem
Let's say an existing codebase uses Python dataclasses as follows:
@dataclass
class Potato:
x: str
y: int
But what if I want to use the sexy new Python library FastAPI to build a REST app? Oooh, so ✨shiny✨. So many 💯emojis❤️ in the 💻git👾 📀commits💾. It feels like Flask. I love Flask. I mean it has to be good right? Linus Torvalds did say "Mo' emojis, mo' performance." Ok, I'm in, let's use it.
Aside from cargo culting FastAPI, why would you want to use it? I would say there are 2 main draws.
- Asynchronous IO - people hate async/await stuff, but it's faster and uses the CPU more efficiently in many cases.
- Swagger/Open Docs - it's awesome to have "self-documenting" rest APIs generated for you out of the box. It's great for testing and debugging, and in startup land, putting out fires in production (whoops!).
Keeping the Dataclass, but Still Using FastAPI
Well, step 1: we need to convert the dataclass object to an object that subclasses the Pydantic BaseModel class. Why? Because that's what FastAPI expects. Well that's not so hard, but what if I'm already using the Potato
class in other places? I would like to preserve that interface so I don't have to go rewriting a bunch of code.
With this constraint the closest thing I have found thus far is to use a Python library called dacite
to avoid having to change the Potato
class from a dataclass to subclassing the Pydantic BaseModel.
from dacite import from_dict, Config
from fastapi import Request
from dataclasses import asdict
from starlette.responses import JSONResponse
router = APIRouter(prefix="")
@router.post("/potato", tags=["Potato"])
async def create_image_metadata(
request: Request,
):
data = await request.json()
if type(data) != dict:
return JSONResponse(status_code=400, content={"message": "Invalid JSON"})
try:
potato = from_dict(
data_class=Potato, data=data, config=Config(cast=[Enum])
)
new_potato = await potato_dao.create_potato(potato)
return asdict(new_potato)
Unfortunately, the down side is you lose out on the auto generated documentation by Swagger/Open Docs. You also don't get any validation that would generally occur with Pydantic type hints. At which point, you basically are now bending over backwards to use FastAPI and get none of the benefits. You may as well just revert back to Starlette, which is what FastAPI is building on.
Ditching the dataclass, and Subclassing Pydantic BaseModel
So, here's where I landed. I wanted to use FastAPI, but I had existing code that defined types using the dataclass decorator. Maybe someone with a bigger brain than I could write a function as follows:
from pydantic.dataclasses import pyd_dataclass
@dataclass
class Potato:
x: str
y: int
tater = Potato(x='bob', y=69420)
# Doesn't work :(
pydantic_tater=pyd_dataclass(tater)
# TODO: magic function
pydantic_tater = dataclass_to_basemodel(tater)
So, I gave up. Instead, I just did the following:
import pydantic import BaseModel
class Potato(BaseModel):
x: str
int: y
And from there I bit the bullet and converted all of the objects that were using dataclass to BaseModel, and changed the interface.