PythonReal Pydantic v2: BaseModel, before/after/wrap validators, ValidationError
from typing import Any
import pydantic
from pydantic import BaseModel, Field, ValidationError, ValidationInfo, field_validator, model_validator
print("pydantic", pydantic.VERSION)
class SignUp(BaseModel):
id: int
name: str
email: str
tags: list[str] = []
age: int = Field(default=0, ge=0, le=150)
password: str
password_again: str
@field_validator("name", mode="before")
@classmethod
def strip_name(cls, v: Any) -> Any:
print(" before(name) got", repr(v))
return v.strip() if isinstance(v, str) else v
@field_validator("name")
@classmethod
def title_name(cls, v: str) -> str:
print(" after(name) got", repr(v))
return v.title()
@field_validator("email", mode="wrap")
@classmethod
def lower_email(cls, v: Any, handler) -> str:
print(" wrap(email) in ", repr(v))
result = handler(v)
print(" wrap(email) out", repr(result))
return result.lower()
@field_validator("password_again")
@classmethod
def same_password(cls, v: str, info: ValidationInfo) -> str:
if v != info.data.get("password"):
raise ValueError("passwords do not match")
return v
@model_validator(mode="after")
def whole_model(self) -> "SignUp":
print(" model after: all fields valid")
return self
print("valid input:")
u = SignUp(id="42", name=" ada lovelace ", email="ADA@Example.ORG",
tags=("math", "engines"), password="pw", password_again="pw")
print(u.model_dump(exclude={"password", "password_again"}))
print(type(u.id).__name__, type(u.tags).__name__)
print("invalid input:")
try:
SignUp(id="4x", name=5, email="e", age=-1, password="a", password_again="b")
except ValidationError as e:
print(e.error_count(), "errors")
for err in e.errors():
print(" ", err["loc"], err["type"], "-", err["msg"])
print("from JSON:")
print(SignUp.model_validate_json(
'{"id": 7, "name": "grace", "email": "G@X.IO", "password": "p", "password_again": "p"}'
).name)
PythonValidator order, strict vs lax, plain mode and validate_assignment
from typing import Annotated
from pydantic import (AfterValidator, BaseModel, BeforeValidator, ConfigDict,
PlainValidator, ValidationError, WrapValidator)
def tag(name):
def check(v):
print(" ", name, repr(v))
return v
return check
def wrap_tag(name):
def check(v, handler):
print(" ", name, "in ", repr(v))
out = handler(v)
print(" ", name, "out", repr(out))
return out
return check
class Order(BaseModel):
x: Annotated[int,
BeforeValidator(tag("before-1")),
AfterValidator(tag("after-1")),
WrapValidator(wrap_tag("wrap-1")),
BeforeValidator(tag("before-2")),
AfterValidator(tag("after-2"))]
print("validator order for x='7':")
Order(x="7")
class Lax(BaseModel):
i: int
f: float
b: bool
class Strict(BaseModel):
model_config = ConfigDict(strict=True)
i: int
print(Lax(i="3", f="1.5", b="yes"))
try:
Lax(i=3.5, f=1, b="maybe")
except ValidationError as e:
print("lax rejects:", [(err["loc"][0], err["type"]) for err in e.errors()])
try:
Strict(i="3")
except ValidationError as e:
print("strict rejects '3':", e.errors()[0]["type"])
print("strict JSON accepts:", Strict.model_validate_json('{"i": 3}'))
class Plain(BaseModel):
x: Annotated[int, PlainValidator(lambda v: v)]
print("plain keeps a str in an int field:", repr(Plain(x="not an int").x))
class Loose(BaseModel):
n: int
class Checked(BaseModel):
model_config = ConfigDict(validate_assignment=True)
n: int
a = Loose(n=1)
a.n = "oops"
print("Loose after assignment:", repr(a.n))
c = Checked(n=1)
c.n = "2"
print("Checked coerced:", repr(c.n))
try:
c.n = "two"
except ValidationError as e:
print("Checked rejects:", e.errors()[0]["type"])
print("model_construct:", repr(Loose.model_construct(n="zz").n))