Initializing a pydantic dataclass from json

Viewed 11899

I'm in the process of converting existing dataclasses in my project to pydantic-dataclasses, I'm using these dataclasses to represent models I need to both encode-to and parse-from json.

Here's an example of my current approach that is not good enough for my use case, I have a class A that I want to both convert into a dict (to later be converted written as json) and to read from that dict. But the only way I can find to parse the json back into a model gives me back the underlying BaseModel and not the dataclass.

note that I'm using the asdict function to convert the dataclass to a dict as it's what the pydantic_encoder uses to convert the dataclass to json, and using the pydantic_encoder what the documentation recommends to convert a pydantic-dataclass to json: https://pydantic-docs.helpmanual.io/usage/dataclasses/

from dataclasses import asdict
from pydantic.dataclasses import dataclass
from pydantic import BaseModel

@dataclass
class A:
    x: str

a = A("string")
a_dict = asdict(a)
parsed_a = A.__pydantic_model__.parse_obj(a_dict)

print(f"type of a: {type(a)}")
print(f"type of parsed_a: {type(parsed_a)}")

print(f"a is instance of A: {isinstance(a, A)}")
print(f"parsed_a is instance of A: {isinstance(parsed_a, A)}")

print(f"a is instance of BaseModel: {isinstance(a, BaseModel)}")
print(f"parsed_a is instance of BaseModel: {isinstance(parsed_a, BaseModel)}")

output:

type of a: <class '__main__.A'>
type of parsed_a: <class '__main__.A'>
a is instance of A: True
parsed_a is instance of A: False
a is instance of BaseModel: False
parsed_a is instance of BaseModel: True

Is there maybe a way to initialize A from the parsed BaseModel?

2 Answers

I think I arrive a little bit late to the party, but I think this answer may come handy for future users having the same question.

To convert the dataclass to json you can use the combination that you are already using using (asdict plus json.dump).

from pydantic.dataclasses import dataclass

@dataclass
class User:
  id: int
  name: str

user = User(id=123, name="James")
d = asdict(user)  # {'id': 123, 'name': 'James'
user_json = json.dumps(d)
print(user_json)  # '{"id": 123, "name": "James"}'

# Or directly with pydantic_encoder
json.dumps(user, default=pydantic_encoder)

Then from the raw json you can use a BaseModel and the parse_raw method.

If you want to deserialize json into pydantic instances, I recommend you using the parse_raw method:

user = User.__pydantic_model__.parse_raw('{"id": 123, "name": "James"}')
print(user)
# id=123 name='James'

Otherwise, if you want to keep the dataclass:

json_raw = '{"id": 123, "name": "James"}'
user_dict = json.loads(json_raw)
user = User(**user_dict)

I managed to address this issue by unpacking the properties of the parsed BaseModel and initializing the dataclass with them.

this solution works recursively for Pydantic dataclasses whose properties are either pydantic-dataclasses or primitives

please note that this solution doesn't work for for Unions and Generics (yet).

 def pydantic_dataclass_from_json_dict(json_dict: dict, pydantic_dataclass_type) -> Any:
    base_model = pydantic_dataclass_type.__pydantic_model__.parse_obj(json_dict)
    base_mode_fields = base_model.__fields__
    dataclass_fields = dataclasses.fields(pydantic_dataclass_type)

    values = []
    for base_model_field_name, base_model_field in base_mode_fields.items():
        value = getattr(base_model, base_model_field_name)
        dataclass_field = [field for field in dataclass_fields if field.name == base_model_field.name][0]
        if is_dataclass(dataclass_field):
            converted_value = pydantic_dataclass_from_json_dict(value, dataclass_field.type)
            values.append(converted_value)
        else:
            values.append(value)

    dataclass_object = pydantic_dataclass_type(*values)
    return dataclass_object
Related