Validate database and Redis URLs with pydantic

A malformed DATABASE_URL does not fail at startup — it fails on the first query, with a stack trace that points at the driver, not the config. A pydantic validator turns it into a clear boot-time error. This page validates connection URLs, extending Custom Validators & Constraints.

Problem 1: any string accepted

# ANTI-PATTERN: a typo'd scheme passes, fails at first connect
class Settings(BaseSettings):
    database_url: str        # "postgres//db" (missing colon) is a valid str
    redis_url: str

The error appears later as an opaque driver exception, far from the bad value.

Problem 2: plaintext where TLS is required

# ANTI-PATTERN: non-TLS Redis accepted in production
redis_url = "redis://cache:6379"   # should be rediss:// in prod

A redis:// URL silently disables TLS where rediss:// was required.

Secure implementation

# config/urls.py
from urllib.parse import urlparse
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(extra="forbid")
    database_url: str
    redis_url: str
    require_tls: bool = True

    @field_validator("database_url")
    @classmethod
    def valid_db(cls, v: str) -> str:
        parsed = urlparse(v)
        if parsed.scheme not in {"postgresql", "postgresql+psycopg", "mysql+pymysql"}:
            raise ValueError(f"unsupported database scheme: {parsed.scheme!r}")
        if not parsed.hostname:
            raise ValueError("database_url has no host")
        return v

    @field_validator("redis_url")
    @classmethod
    def valid_redis(cls, v: str, info) -> str:
        parsed = urlparse(v)
        if parsed.scheme not in {"redis", "rediss"}:
            raise ValueError("redis_url must use redis:// or rediss://")
        if info.data.get("require_tls", True) and parsed.scheme != "rediss":
            raise ValueError("require_tls is set but redis_url is not rediss://")
        return v

urlparse plus a scheme allow-list catches malformed and plaintext URLs at startup; the Redis validator cross-checks require_tls. You can also use pydantic’s PostgresDsn/RedisDsn types for parsing, but a custom validator gives a clearer message and enforces TLS.

Gotchas & version-specific behaviour

  • urlparse does not validate credentials — it only splits the URL; the scheme/host checks do the work.
  • pydantic’s PostgresDsn and RedisDsn parse and normalize, but raise generic errors; custom validators read better in logs.
  • Access sibling fields in a v2 field validator via info.data (only fields validated before it are present — order matters).
  • Never log the full URL on error if it embeds a password; report the scheme/host only.

Production parity checklist

  • DB and Redis URLs validated against a scheme allow-list at startup.
  • TLS enforced (rediss://, sslmode=require) when require_tls is set.
  • extra="forbid" rejects stray connection variables.
  • Error messages avoid printing embedded credentials.
  • A CI fixture exercises valid and invalid URLs.

Conclusion

A scheme allow-list and a TLS cross-check turn a bad connection URL into a precise startup error instead of a runtime mystery. For ARN and webhook validation, see Validators for AWS ARNs and URLs.