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
HttpUrlandPostgresDsn(found inpydantic/networks.py) provide properties forscheme,username,password,host,port,path,query, andfragment. - Multi-Host Support: Types like
PostgresDsnandMongoDsninherit 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.combecomeshttp://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:
- You are using
from __future__ import annotationsif using Python 3.7-3.9. - You call
Model.model_rebuild()after all related models are defined in the namespace. - The string reference in
list['ModelName']exactly matches the class name.