Search code examples
pythonpydantic

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.


Solution

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