Let's cut the fluff. Here is exactly what you need to know about this concept to survive in a real production environment.
1Beyond Basic Types
Look, if you've ever dealt with this in production, you know exactly what the problem is. Pydantic's Field function is great for checking string lengths or integer limits. But what if you have a complex business rule? For example, what if you need to ensure that a user's password does not contain their username? You cannot enforce cross-field logic using just Field. For this, Pydantic provides the @field_validator and @model_validator decorators. This isn't just academic theory—understanding the *why* behind this is what separates junior devs from senior engineers. When you deploy to a cluster, this is the mechanic that prevents catastrophic failure.
# Rule: Password cannot contain username
# User: "alice"
# Pass: "alice123" ⌠Rejected!
# How do we enforce this in Pydantic?
The server returned a 200 OK HTTP response.
2Field Validators
Look, if you've ever dealt with this in production, you know exactly what the problem is. The @field_validator decorator allows you to write a custom Python function that intercepts a specific field's value before it is finalized. If the value breaks your rule, you raise ValueError(). Pydantic catches this error, translates it into a beautiful 422 HTTP response, and sends it back to the client seamlessly. This isn't just academic theory—understanding the *why* behind this is what separates junior devs from senior engineers. When you deploy to a cluster, this is the mechanic that prevents catastrophic failure.
class UserCreate(BaseModel):
username: str
@field_validator('username')
@classmethod
def prevent_admin_name(cls, v: str):
if "admin" in v.lower():
raise ValueError("Username cannot contain 'admin'")
return v
The server returned a 200 OK HTTP response.
3Model Validators
Look, if you've ever dealt with this in production, you know exactly what the problem is. If you need to compare two different fields (like username and password), you cannot use a field validator, because it only looks at one field at a time. Instead, you use @model_validator(mode='after'). This function runs after all individual fields are parsed. It receives the entire object (self), allowing you to cross-reference any combination of fields. This isn't just academic theory—understanding the *why* behind this is what separates junior devs from senior engineers. When you deploy to a cluster, this is the mechanic that prevents catastrophic failure.
class UserCreate(BaseModel):
username: str
password: str
@model_validator(mode='after')
def check_passwords_match(self) -> 'UserCreate':
if self.username.lower() in self.password.lower():
raise ValueError("Password cannot contain username")
return self
The server returned a 200 OK HTTP response.
4Complex Validation Set
Look, if you've ever dealt with this in production, you know exactly what the problem is. By mastering custom validators, you can enforce strict, unbreakable business logic entirely at the schema layer. Your routing endpoints remain perfectly clean, completely unaware of validation logic. Next, we must prepare our database to handle these advanced schemas over time using Alembic migrations. This isn't just academic theory—understanding the *why* behind this is what separates junior devs from senior engineers. When you deploy to a cluster, this is the mechanic that prevents catastrophic failure.
.curriculum { next: 'database_migrations'; }
The server returned a 200 OK HTTP response.
