Chr1s
Chr1s

Reputation: 288

How to parse a pydantic model with a field of type "Type" from json?

How to make the following work with pydantic?

from typing import Type

import pydantic


class InputField(pydantic.BaseModel):
    name: str
    type: Type

InputField.parse_raw('{"name": "myfancyfield", "type": "str"}')

It fails with

pydantic.error_wrappers.ValidationError: 1 validation error for InputField
type
  a class is expected (type=type_error.class)

But I need to parse this from json, so I don't have the option to directly pass the Type object to the __init__ method.

Upvotes: 3

Views: 7707

Answers (2)

Daniil Fajnberg
Daniil Fajnberg

Reputation: 18388

Updated answer (Pydnatic v2)

A custom BeforeValidator will allow you to attempt to find a class with the provided name. Here is a working example first trying to grab a built-in and failing that assuming the class is in global namespace:

from typing import Annotated, Any

from pydantic import BaseModel, BeforeValidator


def ensure_type_instance(v: Any) -> type:
    name = str(v)
    try:
        obj = getattr(__builtins__, name)
    except AttributeError:
        try:
            obj = globals()[name]
        except KeyError:
            raise ValueError(f"{v} is not a valid name")
    if not isinstance(obj, type):
        raise ValueError(f"{obj} is not a class")
    return obj


class InputField(BaseModel):
    name: str
    type_: Annotated[type, BeforeValidator(ensure_type_instance)]


class Foo:
    pass


print(InputField.model_validate_json('{"name": "a", "type_": "str"}'))
print(InputField.model_validate_json('{"name": "a", "type_": "Foo"}'))

Output:

name='a' type_=<class 'str'>
name='b' type_=<class '__main__.Foo'>

Original answer (Pydantic v1)

A custom validator with pre=True will allow you to attempt to find a class with the provided name. Here is a working example first trying to grab a built-in and failing that assuming the class is in global namespace:

from pydantic import BaseModel, validator


class InputField(BaseModel):
    name: str
    type_: type

    @validator("type_", pre=True)
    def parse_cls(cls, value: object) -> type:
        name = str(value)
        try:
            obj = getattr(__builtins__, name)
        except AttributeError:
            try:
                obj = globals()[name]
            except KeyError:
                raise ValueError(f"{value} is not a valid name")
        if not isinstance(obj, type):
            raise TypeError(f"{value} is not a class")
        return obj


class Foo:
    pass


if __name__ == "__main__":
    print(InputField.parse_raw('{"name": "a", "type_": "str"}'))
    print(InputField.parse_raw('{"name": "b", "type_": "Foo"}'))

Output:

name='a' type_=<class 'str'>
name='b' type_=<class '__main__.Foo'>

If you want to support dynamic imports as well, that is possible too. See here or here for pointers.

Upvotes: 3

Eduardo Lucio
Eduardo Lucio

Reputation: 2447

If you want to convert a Pydantic object/type to another Pydantic object/type.

my-test.py test script

from pydantic import BaseModel, Field


# Some hypothetical Pydantics types.
class PyDanticTypeA(BaseModel):
    attribute_a: str
    attribute_b: str


class PyDanticTypeB(PyDanticTypeA):
    attribute_c: str


class PyDanticTypeC(PyDanticTypeA):
    attribute_d: str = Field("d")


# Converting (parsing) one Pydantic type to another.
pydantic_type_b = PyDanticTypeB(attribute_a="a", attribute_b="b", attribute_c="c")
pydantic_type_c = PyDanticTypeC.parse_obj(pydantic_type_b)

# Testing the converted (parsed) Pydantic type.
print(pydantic_type_c.attribute_d)
pydantic_type_c.attribute_d = "e"
print(pydantic_type_c.attribute_d)

Running my-test.py

(my-test) [who-i-am@who-i-am-pc my-test]$ python my-test.py 
d
e

Thanks!

āœŒļøšŸ˜Š

Upvotes: 0

Related Questions