pythonpydanticpydantic-v2

"A custom validator is returning a value other than `self`." when using @model_validator(mode="wrap")


This class can be used either within highly nested JSON, or dynamically created by program code.

class CriticalDefinition(BaseModel):
    color: Color
    value: int

    @model_validator(mode="wrap")
    @classmethod
    def _alias_replacer(cls, v: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
        global _alias
        if isinstance(v, str) and (r := _alias.get(v)):
            return r
        if isinstance(v, dict) and "_alias" in v and (r := _alias.get(v["_alias"])):
            return r
        return handler(v)

I defined some commonly used CriticalDefinition object as alias. For example, instead of critical: {color: "BLUE", value: 50} on JSON, I might write critical: "BLUE_50", and this works as intended.

The problem is, when I tried to write c = CriticalDefinition(_alias="BLUE_50") on python code, I got follow warning:

UserWarning: A custom validator is returning a value other than `self`.
Returning anything other than `self` from a top level model validator isn't supported when validating via `__init__`.
See the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.
  c = CriticalDefinition(_alias="BLUE_50")

the result object, c, is not what expected; it seems to completely empty.

The document provided by link says this:

Wrap validators: are the most flexible of all. You can run code before or after Pydantic and other validators process the input data, or you can terminate validation immediately, either by returning the data early or by raising an error.

I think I wrote code just as document said (i.e. I returned the data early). But I don't know what I did wrong.


Solution

  • It's probably not very clear from docs, but if you look at code example for model_validator with mode="wrap", you will see that return type annotation is Self: https://docs.pydantic.dev/latest/concepts/validators/#model-validators

    So, you should modify input data and still call handler(v):

    from typing import Any
    
    from pydantic import BaseModel, ModelWrapValidatorHandler, model_validator
    from pydantic_extra_types.color import Color
    from typing_extensions import Self
    
    _alias: dict[str, tuple[Color, int]] = {
        "BLUE_50": {"color": "BLUE", "value": 50},
    }
    
    
    class CriticalDefinition(BaseModel):
        color: Color
        value: int
    
        @model_validator(mode="wrap")
        @classmethod
        def _alias_replacer(cls, v: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
            global _alias
            if isinstance(v, str) and (r := _alias.get(v)):
                v = r
            elif isinstance(v, dict) and "_alias" in v and (r := _alias.get(v["_alias"])):
                v = r
            return handler(v)
    
    
    
    c = CriticalDefinition(_alias="BLUE_50")
    print(c)