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

  1. API Response: Receive raw JSON from API

  2. Type Inference: Analyze response structure

  3. Model Creation: Generate Pydantic model class

  4. Instance Creation: Create validated model instance

  5. 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 * versesVerse model * download_urlsDownloadUrl model * user_idsUserId model

  • Nested 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

  1. Use Protocols: Define Protocols for better type checking

  2. Handle Missing Fields: Use optional access for dynamic fields

  3. Validate Early: Check data structure before processing

  4. Cache Results: Cache processed data to avoid re-processing

  5. 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]