Custom Validators & Constraints
Basic types reject "abc" where an integer is expected, but they happily accept "arn:aws:s3:::" as a string even though it is a malformed ARN. Domain validation is the difference between configuration that is typed and configuration that is correct. This page encodes domain rules into the settings model so a bad value can never reach business logic.
Custom validators extend the settings fundamentals within the type-safe validation section: where field types stop, validators continue.
Secure implementation
# config/validated.py
import re
from pydantic import Field, field_validator, model_validator, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
ARN_RE = re.compile(r"^arn:aws:[a-z0-9-]+:[a-z0-9-]*:\d{12}:.+$") # anchored, no nested quantifiers
class ResourceSettings(BaseSettings):
model_config = SettingsConfigDict(extra="forbid")
queue_arn: str
callback_url: str
signing_key: SecretStr
require_tls: bool = True
@field_validator("queue_arn")
@classmethod
def valid_arn(cls, v: str) -> str:
if not ARN_RE.match(v):
raise ValueError("queue_arn is not a well-formed AWS ARN")
return v
@field_validator("callback_url")
@classmethod
def https_only(cls, v: str) -> str:
if not v.startswith("https://"):
raise ValueError("callback_url must use https")
return v
@model_validator(mode="after")
def tls_consistency(self) -> "ResourceSettings":
if self.require_tls and not self.callback_url.startswith("https://"):
raise ValueError("require_tls is set but callback_url is not https")
return self
Field-level validators check one value; the model validator enforces relationships between fields. Both run at construction, so the model cannot be built with an invalid combination.
Configuration reference
| Tool | Scope | Runs | Use for |
|---|---|---|---|
Field(ge=, le=, max_length=) |
one field | parse | Numeric/length bounds |
Field(pattern=) |
one field | parse | Simple anchored regex |
@field_validator |
one field | after parse | Logic, normalization, custom errors |
@model_validator(mode="after") |
whole model | after all fields | Cross-field rules |
SecretStr |
one field | — | Mask credential fields |
Deployment parity: local to production
- Local dev — invalid ARNs or non-TLS URLs fail at construction on the developer’s machine.
- CI — a fixture that builds the model with representative values guards every validator.
- Staging/Production — the same validators run at boot; a malformed injected value stops the rollout, not a request.
Security boundaries & guardrails
- Anchor every regex (
^...$) and avoid nested quantifiers to prevent ReDoS. - Prefer explicit prefix/scheme checks over broad patterns for ARNs and URLs.
- Keep
extra="forbid"so unvalidated extra keys cannot slip through. - Validate, never sanitize-and-continue — reject bad config rather than silently repairing it.
- Wrap signing keys and tokens in
SecretStreven inside validated models.
Troubleshooting
ValidationErrorlists multiple fields — pydantic collects all failures; fix them together.- Validator not running — missing
@classmethodunder@field_validator, or the method referencesselfinstead ofcls. - Regex hangs on long input — catastrophic backtracking; re-anchor and simplify the pattern. See Validators for AWS ARNs and URLs.
- Cross-field rule ignored — it belongs in
@model_validator(mode="after"), not a field validator.
Frequently asked questions
When should I use Field constraints versus a field_validator?
Use Field(ge=..., max_length=..., pattern=...) for simple declarative bounds, and a @field_validator when the rule needs Python logic — cross-checking, normalizing, or a domain-specific error message.
How do I validate one field against another?
Use a @model_validator(mode="after"), which runs once all fields are populated and can compare them — for example, asserting a TLS cert path is set whenever ssl is True.
Are regex patterns in validators a security risk?
A poorly written pattern can be vulnerable to catastrophic backtracking (ReDoS). Anchor patterns, avoid nested quantifiers, and prefer explicit prefix checks over broad regexes for inputs like ARNs and URLs.
Conclusion
The invariant: every domain rule lives inside the model as a validator, runs at construction, and rejects rather than repairs. Configuration that builds is configuration that is correct.