Skip to main content

Specialized & Recursive Schemas

Pydantic provides robust support for specialized data types, fine-grained constraints via metadata, and recursive model definitions. These features leverage pydantic-core for high-performance validation while maintaining a clean Pythonic API.

Using Specialized Network Types

Pydantic includes specialized types for URLs and Database Source Names (DSNs) in the pydantic.networks module. These types provide automatic validation and convenient access to URL components.

from pydantic import BaseModel, HttpUrl, PostgresDsn, ValidationError

class ConnectionSettings(BaseModel):
website: HttpUrl
database_url: PostgresDsn

# Validation and parsing
settings = ConnectionSettings(
website="https://example.com/path?query=1",
database_url="postgresql://user:pass@localhost:5432/db"
)

print(settings.website.host) # example.com
print(settings.website.scheme) # https
print(settings.database_url.port) # 5432

Key Features of Network Types

  • Component Access: Classes like HttpUrl and PostgresDsn (found in pydantic/networks.py) provide properties for scheme, username, password, host, port, path, query, and fragment.
  • Multi-Host Support: Types like PostgresDsn and MongoDsn inherit from _BaseMultiHostUrl, allowing them to handle multiple hosts (e.g., for database clusters).
  • Punycode Encoding: International domains are automatically converted to punycode (e.g., http://puny£code.com becomes http://xn--punycode-eja.com/).

Applying String and Numeric Constraints

In Pydantic V2, constraints are primarily applied using Annotated with metadata classes like StringConstraints or Field. This approach is preferred over the legacy constr and conint functions.

String Constraints

Use StringConstraints to enforce length, patterns, and formatting.

from typing import Annotated
from pydantic import BaseModel, StringConstraints

class User(BaseModel):
# Username must be 3-20 chars, lowercase, and match a regex
username: Annotated[
str,
StringConstraints(
min_length=3,
max_length=20,
to_lower=True,
pattern=r'^[a-z0-9_-]+$'
)
]

user = User(username=" John_Doe ")
print(user.username) # "john_doe" (stripped and lowercased if strip_whitespace=True was set)

Numeric and UUID Constraints

Pydantic provides pre-defined constrained types in pydantic.types for common scenarios.

from uuid import UUID
from pydantic import BaseModel, PositiveInt, UUID4

class Profile(BaseModel):
age: PositiveInt # Equivalent to Annotated[int, Gt(0)]
user_id: UUID4 # Annotated[UUID, UuidVersion(4)]

# You can also use Field for custom numeric constraints
from pydantic import Field

class Product(BaseModel):
price: Annotated[float, Field(gt=0, le=1000)]

Defining Recursive Schemas

Recursive schemas allow a model to contain references to itself. This is common for tree structures, nested comments, or organizational hierarchies.

Self-Referencing Models

To define a recursive model, use a string forward reference for the type hint.

from pydantic import BaseModel

class Category(BaseModel):
name: str
subcategories: list['Category'] = []

# Usage
root = Category(
name="Electronics",
subcategories=[
Category(name="Laptops"),
Category(name="Smartphones")
]
)

Manual Schema Rebuilding

In complex scenarios—such as circular dependencies between models in different modules—Pydantic might not be able to resolve forward references automatically. In these cases, use the model_rebuild() method.

from pydantic import BaseModel

class Node(BaseModel):
value: int
parent: 'Node | None' = None

# If Node was defined in a way that delayed resolution:
Node.model_rebuild()

Troubleshooting and Gotchas

Underscores in Hostnames

By default, Pydantic's URL types allow underscores in hostnames (except in the TLD). This matches the behavior of modern browsers but may differ from strict RFC interpretations. If you need stricter validation, use a custom validator.

Discouraged con* Functions

Functions like constr(), conint(), and conlist() are discouraged in V2 and will be deprecated in V3. They return a type dynamically, which can confuse static analysis tools like Mypy or Pyright. Always prefer Annotated[type, StringConstraints(...)] or Annotated[type, Field(...)].

Recursive Resolution Depth

If you encounter errors regarding "ForwardRef not resolved", ensure that:

  1. You are using from __future__ import annotations if using Python 3.7-3.9.
  2. You call Model.model_rebuild() after all related models are defined in the namespace.
  3. The string reference in list['ModelName'] exactly matches the class name.