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

  1. Local dev — invalid ARNs or non-TLS URLs fail at construction on the developer’s machine.
  2. CI — a fixture that builds the model with representative values guards every validator.
  3. 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 SecretStr even inside validated models.

Troubleshooting

  • ValidationError lists multiple fields — pydantic collects all failures; fix them together.
  • Validator not running — missing @classmethod under @field_validator, or the method references self instead of cls.
  • 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.