I have a partial solution / workaround here. The problem actually seems deceptively complicated and may take some work for a really complete solution.
You see that this works as expected in the simple case with no nested dataclass models - however if you have nested dataclass models you will need to pass them into the pydantic model as dataclasses (or dump them as dicts and allow pydantic to do the conversion) in this implementation, which is a little bit unwieldy.
Have left a note as to how to improve - but will take some work to do the nitty gritty of it. Hopefully this is a good starting point and let you do what you need to do. If I have a spare moment at some time I may try and improve this code to change the nested model type later.
As a side note would say that in my own project I am ok with just writing the classes twice. Once as dataclasses and once as pydantic schema models. The reason is that the business logic should not be tightly coupled with what the schema returns - and usuaually you will find your internal model may start the same but will at some point will likely diverge anyway. ie you will want fields on your dataclass that are for backend use only and derived fields on your schema etc.
from typing import Type, Any, Dict
from pydantic import BaseModel, create_model
from dataclasses import fields, MISSING, dataclass
def camel_case_converter(value: str) -> str:
parts = value.lower().split('_')
return parts[0] + ''.join(i.title() for i in parts[1:])
class CamelBaseModel(BaseModel):
class Config:
alias_generator = camel_case_converter
populate_by_name = True
def model_from_dataclass(kls: 'StdlibDataclass') -> Type[BaseModel]:
"""Converts a stdlib dataclass to a pydantic BaseModel"""
field_definitions: Dict[str, Any] = {}
for field in fields(kls):
field_type = field.type
# add recursive functionality for nested dataclasses could get a bit tricky
# for example field type `list[Item]` ideally needs to be evaluated and converted
# to `list[ItemSchema]` there are workarounds though see below
default_value = field.default if field.default is not MISSING else ...
field_definitions[field.name] = (field_type, default_value)
model = create_model(
kls.__name__,
__base__=CamelBaseModel,
**field_definitions
)
return model
@dataclass
class Item:
id: int | None = None
stuff: str | None = None
height: float | None = None
@dataclass
class Bag:
id: int | None = None
name: str | None = None
things: list[Item] | None = None
@dataclass
class Basket:
id: int | None = None
recipient: str | None = None
bags: list[Bag] | None = None
best_item: Item | None = None
ItemSchema = model_from_dataclass(Item)
BagSchema = model_from_dataclass(Bag)
BasketSchema = model_from_dataclass(Basket)
# Testing the model
item = ItemSchema(id=1, stuff="example", height=5.9)
# will not work because `item` is not a dataclass
# bag = BagSchema(id=1, name="example", things=[item])
# this will work
item_dataclass = Item(id=1, stuff="example", height=5.9)
bag = BagSchema.model_validate({
'id': 1, 'name': "example", 'things': [item_dataclass]
})
# this will also work
bag = BagSchema.model_validate({
'id': 1,
'name': 'example',
'things': [item.model_dump()]
})
print(bag)
# output: id=1 name='example' things=[Item(id=1, stuff='example', height=5.9)