Search code examples
pythonpython-3.xpydantic

Pydantic: Transform a value before it is assigned to a field?


I have the following model

class Window(BaseModel):
    size: tuple[int, int]

and I would like to instantiate it like this:

fields = {'size': '1920x1080'}
window = Window(**fields)

Of course this fails since the value of 'size' is not of the correct type. However, I would like to add logic so that the value is split at x, i.e.:

def transform(raw: str) -> tuple[int, int]:
    x, y = raw.split('x')
    return int(x), int(y)

Does Pydantic support this?


Solution

  • Pydantic 2.x (edit)

    Pydantic 2.0 introduced the field_validator decorator which lets you implement such a behaviour in a very simple way. Given the original parsing function:

    from pydantic import BaseModel, field_validator
    
    class Window(BaseModel):
        size: tuple[int, int]
    
        @field_validator("size", mode="before")
        @classmethod
        def transform(cls, raw: str) -> tuple[int, int]:
            x, y = raw.split("x")
            return int(x), int(y)
    

    Note:

    • The validator method is a class method, as denoted by the cls first argument. Implementing it as an instance method (with self) will raise an error.
    • The mode="before" in the decorator is critical here, as expected this is what makes the method run before checking "size" is a tuple.

    Pydantic 1.x (original answer)

    You can implement such a behaviour with pydantic's validator. Given your predefined function:

    def transform(raw: str) -> tuple[int, int]:
        x, y = raw.split('x')
        return int(x), int(y)
    

    You can implement it in your class like this:

    from pydantic import BaseModel, validator
    
    
    class Window(BaseModel):
        
        size: tuple[int, int]
        _extract_size = validator('size', pre=True, allow_reuse=True)(transform)
    
    

    Note the pre=True argument passed to the validator. It means that it will be run before the default validator that checks if size is a tuple.


    Now:

    fields = {'size': '1920x1080'}
    window = Window(**fields)
    print(window)
    # output: size=(1920, 1080)
    

    Note that after that, you won't be able to instantiate your Window with a tuple for size.

    fields2 = {'size': (800, 600)}
    window2 = Window(**fields2)
    # AttributeError: 'tuple' object has no attribute 'split'
    

    In order to overcome that, you could simply bypass the function if a tuple is passed by altering slightly your code:

    Pydantic 2.x

    class Window(BaseModel):
        size: tuple[int, int]
    
        @field_validator("size", mode="before")
        def transform(cls, raw: str | tuple[int, int]) -> tuple[int, int]:
            if isinstance(raw, tuple):
                return raw
            x, y = raw.split("x")
            return int(x), int(y)
    

    Pydantic 1.x

    def transform(raw: str | tuple[int, int]) -> tuple[int, int]:
        if isinstance(raw, tuple):
            return raw
        x, y = raw.split('x')
        return int(x), int(y)
    
    class Window(BaseModel):
    
        size: tuple[int, int]
        _extract_size = validator('size', pre=True, allow_reuse=True)(transform)
    

    Which should give:

    fields2 = {'size': (800, 600)}
    window2 = Window(**fields2)
    print(window2)
    # output: size:(800, 600)