Dynamic Pydantic Models
The YouVersion Bible Client uses dynamic Pydantic model generation to automatically create type-safe models from API responses.
How It Works
Instead of pre-defining models for every possible API response structure, the client dynamically generates Pydantic models at runtime based on the actual API response data.
Benefits
Flexibility: Automatically adapts to API response changes
Type Safety: Still provides type checking and validation
No Manual Updates: Models update automatically with API changes
Memory Efficient: Models are cached and reused
Model Generation Process
API Response: Receive raw JSON from API
Type Inference: Analyze response structure
Model Creation: Generate Pydantic model class
Instance Creation: Create validated model instance
Caching: Cache model classes for reuse
Example
When you call an API method:
from youversion.clients import SyncClient
with SyncClient() as client:
# API returns raw JSON
# Client automatically creates a Pydantic model
moment = client.moments()[0]
# moment is a dynamically created Pydantic model
print(type(moment)) # <class 'pydantic.main.Moment_...'>
print(moment.id)
print(moment.moment_title)
Model Naming
Models are named based on their context:
List Elements: Field names are converted to PascalCase *
verses→Versemodel *download_urls→DownloadUrlmodel *user_ids→UserIdmodelNested Models: Created recursively for nested structures
Example:
# API response structure:
{
"results": [
{
"verses": [
{"text": "...", "reference": "..."}
]
}
]
}
# Generated models:
# - Results (for list items)
# - Verse (for verses list items)
# - Each with proper type hints
Accessing Dynamic Models
Dynamic models behave like regular Pydantic models:
from youversion.clients import SyncClient
with SyncClient() as client:
moments = client.moments()
for moment in moments:
# Access attributes
print(moment.id)
print(moment.moment_title)
# Convert to dict
data = moment.model_dump()
# Convert to JSON
json_data = moment.model_dump_json()
# Validate (already validated on creation)
assert isinstance(moment, type(moment))
Type Checking
While models are dynamic, you can still use type hints:
from typing import Any, List
from youversion.clients import AsyncClient
async def process_moments() -> List[Any]:
async with AsyncClient() as client:
moments = await client.moments()
return moments
# Or use Protocols for better type checking
from youversion.models.base import Moment
async def process_moments() -> List[Moment]:
async with AsyncClient() as client:
moments = await client.moments()
return moments
Model Caching
Models are cached to improve performance:
from youversion.clients import SyncClient
with SyncClient() as client:
# First call - creates and caches model
moments1 = client.moments(page=1)
# Second call - reuses cached model
moments2 = client.moments(page=2)
# Same model class is used
assert type(moments1[0]) == type(moments2[0])
Nested Structures
Dynamic models handle nested structures automatically:
from youversion.clients import SyncClient
with SyncClient() as client:
# Search results with nested verses
results = client.search_bible("love")
# Nested models are created automatically
for result in results.get("results", []):
for verse in result.get("verses", []):
# verse is a dynamically created Pydantic model
print(verse.text)
print(verse.reference)
Validation
Dynamic models still provide Pydantic validation:
from youversion.clients import SyncClient
from pydantic import ValidationError
with SyncClient() as client:
try:
moments = client.moments()
# All moments are validated
except ValidationError as e:
print(f"Validation error: {e}")
Serialization
Dynamic models support all Pydantic serialization methods:
from youversion.clients import SyncClient
with SyncClient() as client:
moment = client.moments()[0]
# Convert to dict
data = moment.model_dump()
# Convert to JSON string
json_str = moment.model_dump_json()
# Convert with exclusions
minimal = moment.model_dump(exclude={'user', 'actions'})
# Convert with only specific fields
limited = moment.model_dump(include={'id', 'moment_title'})
Limitations
No Static Type Checking: Models are created at runtime
IDE Support: Limited autocomplete for dynamic models
Documentation: Model structure not known until runtime
Workarounds
Use Protocols for Better IDE Support
Define Protocols for better type hints:
from typing import Protocol
from youversion.models.base import MomentProtocol
def process_moment(moment: MomentProtocol) -> str:
"""Process a moment with type checking."""
return f"{moment.id}: {moment.moment_title}"
from youversion.clients import SyncClient
with SyncClient() as client:
moments = client.moments()
for moment in moments:
result = process_moment(moment) # Type checked!
Inspect Model Structure
Inspect model fields at runtime:
from youversion.clients import SyncClient
with SyncClient() as client:
moment = client.moments()[0]
# Get model fields
fields = moment.model_fields
for field_name, field_info in fields.items():
print(f"{field_name}: {field_info.annotation}")
Best Practices
Use Protocols: Define Protocols for better type checking
Handle Missing Fields: Use optional access for dynamic fields
Validate Early: Check data structure before processing
Cache Results: Cache processed data to avoid re-processing
Document Assumptions: Document expected model structure
Example: Working with Dynamic Models
from youversion.clients import SyncClient
from typing import Any
def process_dynamic_moment(moment: Any) -> dict:
"""Process a dynamically created moment model."""
# Safely access fields
data = {
'id': getattr(moment, 'id', None),
'title': getattr(moment, 'moment_title', 'Untitled'),
'kind': getattr(moment, 'kind_id', 'unknown'),
}
# Convert to dict for easier manipulation
full_data = moment.model_dump()
# Merge with custom data
return {**full_data, **data}
with SyncClient() as client:
moments = client.moments()
processed = [process_dynamic_moment(m) for m in moments]