> ## Documentation Index
> Fetch the complete documentation index at: https://tif1.tracinginsights.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedule Schema & Validation

> Comprehensive guide to schedule data validation, schema management, and internal data structures for Formula 1 event and session information

# Schedule Schema & Validation

The `schedule_schema` module serves as the foundational validation layer for tif1's internal schedule data architecture. This module implements a comprehensive validation system that acts as a critical gatekeeper, ensuring data integrity and structural consistency across all schedule-related operations within the library.

## Purpose and Role

The schedule validation system is designed to guarantee that all schedule payloads—regardless of their origin (vendored JSON files, CDN sources, or custom data)—conform precisely to tif1's expected internal schema before being consumed by higher-level APIs. This validation layer provides several critical functions:

* **Data Integrity Assurance**: Validates that schedule data structures are complete, correctly typed, and internally consistent
* **Early Error Detection**: Catches malformed or incomplete data at the earliest possible stage, preventing cascading failures
* **Schema Version Management**: Ensures compatibility between data format versions and library expectations
* **Type Safety Enforcement**: Verifies that all data elements match their expected types throughout the hierarchy
* **Consistency Guarantees**: Ensures uniform data structure across all years, events, and data sources

The validation process is designed for performance, typically completing in under 1 millisecond, and integrates seamlessly with tif1's caching system to ensure validation overhead is minimized in production use.

<Note>
  **For Most Users**: This module operates transparently behind the scenes. You'll interact with schedule data through high-level APIs like `get_events()`, `get_sessions()`, and `get_event_schedule()`, which automatically handle validation. Direct use of validation functions is typically only needed for advanced scenarios such as working with custom schedule data, debugging data issues, or extending the library.
</Note>

<Warning>
  **Schema Version Compatibility**: Currently, only schema version 1 is supported. Attempting to validate payloads with different schema versions will raise an `InvalidDataError`. Future versions of tif1 may introduce new schema versions with additional features or structural changes.
</Warning>

***

## Architecture Overview

### Data Flow Pipeline

The schedule validation system sits at a critical junction in tif1's data pipeline. Understanding this flow helps clarify when and why validation occurs:

```
┌─────────────────────────────────────────────────────────────────┐
│                     Schedule Data Sources                        │
├─────────────────────────────────────────────────────────────────┤
│  1. Vendored JSON Files (src/tif1/data/schedules/f1schedule/)   │
│     • Per-year files: schedule_2021.json, schedule_2022.json    │
│     • Columnar format (pandas-like structure)                    │
│     • Bundled with library installation                          │
│                                                                   │
│  2. CDN Fallback (jsdelivr)                                      │
│     • Fetched for years not in vendored data                     │
│     • Same columnar format as vendored files                     │
│     • Cached after first successful fetch                        │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                    Format Conversion Layer                       │
├─────────────────────────────────────────────────────────────────┤
│  _convert_f1schedule_year()                                      │
│  • Transforms columnar format → event-centric structure          │
│  • Extracts metadata (dates, locations, formats)                 │
│  • Builds sessions mapping                                       │
│  • Sorts events by round number                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│              ★ VALIDATION LAYER (This Module) ★                  │
├─────────────────────────────────────────────────────────────────┤
│  validate_schedule_payload()                                     │
│  • Verifies schema version compatibility                         │
│  • Validates structural integrity                                │
│  • Checks type correctness                                       │
│  • Ensures event-session consistency                             │
│  • Provides detailed error messages                              │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                      Caching & Storage                           │
├─────────────────────────────────────────────────────────────────┤
│  • Validated payload cached via @lru_cache                       │
│  • Single validation per Python session                          │
│  • Instant access for subsequent requests                        │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                    High-Level Public APIs                        │
├─────────────────────────────────────────────────────────────────┤
│  • get_events(year) → List of event names                        │
│  • get_sessions(year, event) → List of session names             │
│  • get_event_schedule(year) → EventSchedule DataFrame            │
│  • get_event(year, identifier) → Event object                    │
└─────────────────────────────────────────────────────────────────┘
```

### Key Design Principles

1. **Fail-Fast Philosophy**: Validation occurs immediately after data conversion, before any caching or API exposure. This ensures that invalid data never propagates through the system.

2. **Single Validation Point**: All schedule data, regardless of source, passes through the same validation function, ensuring consistent quality standards.

3. **Performance Optimization**: Validation is designed to be extremely fast (typically \<1ms) and runs only once per session due to aggressive caching.

4. **Detailed Error Reporting**: When validation fails, the system provides specific, actionable error messages that pinpoint exactly what went wrong and where.

5. **Type Safety**: The validation system enforces strict type checking at every level of the data hierarchy, preventing type-related bugs downstream.

### Validation Scope

The validation system checks multiple aspects of schedule data:

| Validation Category     | What It Checks                                  | Why It Matters                                  |
| ----------------------- | ----------------------------------------------- | ----------------------------------------------- |
| **Top-Level Structure** | Payload is a dictionary with required keys      | Prevents type errors in downstream code         |
| **Schema Version**      | Version number is supported (currently only v1) | Ensures compatibility with library expectations |
| **Year Keys**           | Year keys are numeric strings (e.g., "2021")    | Enables proper sorting and year-based lookups   |
| **Events List**         | Each year has a list of non-empty event names   | Guarantees event discovery APIs work correctly  |
| **Sessions Mapping**    | Each event has a corresponding session list     | Ensures session queries return valid data       |
| **Session Names**       | All session names are non-empty strings         | Prevents empty or null session references       |
| **Consistency**         | Every event in the events list has sessions     | Maintains referential integrity                 |

### Performance Characteristics

The validation system is optimized for production use:

* **Time Complexity**: O(n × m) where n = number of years, m = average events per year
* **Typical Runtime**: \<1ms for standard multi-year schedules (5-10 years)
* **Memory Overhead**: Minimal; validates in-place without copying data structures
* **Caching Strategy**: Results cached via `@lru_cache`; validation runs once per Python session
* **Scalability**: Handles schedules with 20+ events per year efficiently

<Info>
  **Benchmark Example**: Validating a 5-year schedule (2020-2024) with \~22 events per year and \~5 sessions per event typically completes in 0.3-0.8ms on modern hardware.
</Info>

***

## Core API Reference

### `validate_schedule_payload`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def validate_schedule_payload(payload: Any) -> dict[str, Any]
```

The primary and only public validation function in this module. This function performs comprehensive, hierarchical validation of schedule payloads to ensure they conform to tif1's internal schema specification version 1.

#### Function Signature

**Parameters:**

* **`payload`** (`Any`): The decoded schedule payload to validate. While typed as `Any` to accept arbitrary input, the function expects a dictionary with the following structure:
  ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
      "schema_version": int,  # Must be exactly 1
      "years": {
          "YYYY": {  # Year as string, e.g., "2021", "2024"
              "events": [str, ...],  # List of event names
              "sessions": {
                  "Event Name": [str, ...]  # Session names for each event
              },
              "metadata": {  # Optional metadata dictionary
                  "Event Name": {
                      "RoundNumber": int,
                      "EventDate": str,
                      "Location": str,
                      # ... additional metadata fields
                  }
              }
          }
      }
  }
  ```

**Returns:**

* **`dict[str, Any]`**: The validated payload, returned unchanged if all validation checks pass. This design allows for method chaining and confirms that the payload is safe for use in downstream operations. The return value is guaranteed to match the expected schema structure.

**Raises:**

* **`InvalidDataError`**: Raised when any validation check fails. The exception includes:
  * **`message`**: Human-readable description of the validation failure
  * **`context`**: Dictionary containing structured error information, including:
    * **`reason`**: Detailed explanation of what validation rule was violated
    * Additional context-specific fields depending on the failure type

#### Validation Process

The function performs validation in a hierarchical, top-down manner, checking each level of the data structure before proceeding to the next:

##### Level 1: Top-Level Structure Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Checks performed:
- payload is a dictionary (not list, string, None, etc.)
- payload contains "schema_version" key
- payload contains "years" key
```

**Failure Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Invalid: payload is a list
payload = [{"year": 2021}]
# Raises: InvalidDataError(reason="Schedule payload must be an object")

# Invalid: missing required keys
payload = {"data": {...}}
# Raises: InvalidDataError(reason="Schedule payload missing 'years' object")
```

##### Level 2: Schema Version Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Checks performed:
- schema_version exists
- schema_version is exactly 1 (only supported version)
```

**Failure Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Invalid: unsupported version
payload = {"schema_version": 2, "years": {...}}
# Raises: InvalidDataError(reason="Unsupported schedule schema version: 2")

# Invalid: missing version
payload = {"years": {...}}
# Raises: InvalidDataError(reason="Unsupported schedule schema version: None")
```

##### Level 3: Years Container Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Checks performed:
- "years" value is a dictionary
- Each year key is a string of digits (e.g., "2021", not 2021 or "twenty-one")
- Each year value is a dictionary
```

**Failure Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Invalid: year key is not a string of digits
payload = {
    "schema_version": 1,
    "years": {
        "twenty-twenty-one": {...}  # Should be "2021"
    }
}
# Raises: InvalidDataError(reason="Invalid year key: 'twenty-twenty-one'")

# Invalid: year value is not a dictionary
payload = {
    "schema_version": 1,
    "years": {
        "2021": ["event1", "event2"]  # Should be a dict
    }
}
# Raises: InvalidDataError(reason="Year payload must be object for year=2021")
```

##### Level 4: Per-Year Structure Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# For each year, checks:
- "events" key exists and is a list
- All items in "events" list are non-empty strings
- "sessions" key exists and is a dictionary
```

**Failure Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Invalid: events is not a list
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": "Bahrain Grand Prix",  # Should be a list
            "sessions": {...}
        }
    }
}
# Raises: InvalidDataError(reason="Invalid events list for year=2021")

# Invalid: event name is empty
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", ""],  # Empty string not allowed
            "sessions": {...}
        }
    }
}
# Raises: InvalidDataError(reason="Invalid events list for year=2021")
```

##### Level 5: Session Mapping Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# For each event in each year, checks:
- Event name exists as a key in the "sessions" dictionary
- Session list for the event is a list
- All session names in the list are non-empty strings
```

**Failure Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Invalid: missing session list for an event
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix" entry!
            }
        }
    }
}
# Raises: InvalidDataError(
#     reason="Invalid session list for year=2021 event='Belgian Grand Prix'"
# )

# Invalid: session list contains non-string
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", None, "Race"]  # None not allowed
            }
        }
    }
}
# Raises: InvalidDataError(
#     reason="Invalid session list for year=2021 event='Bahrain Grand Prix'"
# )
```

#### Usage Examples

##### Example 1: Basic Validation (Success Case)

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Construct a valid payload
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

# Validate the payload
validated = validate_schedule_payload(payload)

# If we reach here, validation succeeded
print(f"✓ Schedule is valid")
print(f"  Years: {list(validated['years'].keys())}")
print(f"  Events in 2021: {len(validated['years']['2021']['events'])}")

# Output:
# ✓ Schedule is valid
#   Years: ['2021']
#   Events in 2021: 2
```

##### Example 2: Handling Validation Errors

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# Invalid payload - missing sessions for an event
invalid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix" sessions!
            }
        }
    }
}

try:
    validate_schedule_payload(invalid_payload)
    print("Validation succeeded")  # Won't reach here
except InvalidDataError as e:
    print(f"✗ Validation failed!")
    print(f"  Error: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")

    # You can also access the full context
    print(f"  Full context: {e.context}")

# Output:
# ✗ Validation failed!
#   Error: Invalid data: Invalid session list for year=2021 event='Belgian Grand Prix'
#   Reason: Invalid session list for year=2021 event='Belgian Grand Prix'
#   Full context: {'reason': "Invalid session list for year=2021 event='Belgian Grand Prix'"}
```

##### Example 3: Multi-Year Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Validate multiple years at once
multi_year_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Abu Dhabi Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Abu Dhabi Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        },
        "2022": {
            "events": ["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Saudi Arabian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        },
        "2023": {
            "events": ["Bahrain Grand Prix", "Saudi Arabian Grand Prix", "Australian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Saudi Arabian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Australian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

validated = validate_schedule_payload(multi_year_payload)

print(f"✓ Validated {len(validated['years'])} years of schedule data")
for year in sorted(validated['years'].keys()):
    events_count = len(validated['years'][year]['events'])
    print(f"  {year}: {events_count} events")

# Output:
# ✓ Validated 3 years of schedule data
#   2021: 2 events
#   2022: 2 events
#   2023: 3 events
```

##### Example 4: Validating Sprint Weekend Format

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Sprint weekends have different session structures
sprint_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["British Grand Prix"],  # Sprint weekend
            "sessions": {
                "British Grand Prix": [
                    "Practice 1",      # Friday
                    "Qualifying",      # Friday (for Sunday grid)
                    "Practice 2",      # Saturday
                    "Sprint",          # Saturday (sets Sunday grid)
                    "Race"             # Sunday
                ]
            }
        }
    }
}

validated = validate_schedule_payload(sprint_payload)
sessions = validated['years']['2021']['sessions']['British Grand Prix']

print(f"✓ Sprint weekend validated")
print(f"  Sessions: {', '.join(sessions)}")
print(f"  Total sessions: {len(sessions)}")

# Output:
# ✓ Sprint weekend validated
#   Sessions: Practice 1, Qualifying, Practice 2, Sprint, Race
#   Total sessions: 5
```

##### Example 5: Validation with Metadata (Optional Fields)

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Metadata is optional and not validated by this function
# (it's validated by higher-level code if present)
payload_with_metadata = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Belgian Grand Prix"],
            "sessions": {
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            },
            "metadata": {  # Optional metadata section
                "Belgian Grand Prix": {
                    "RoundNumber": 12,
                    "EventDate": "2021-08-29T17:00:00",
                    "Location": "Spa-Francorchamps",
                    "Country": "Belgium",
                    "OfficialEventName": "FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021",
                    "EventFormat": "conventional",
                    "GmtOffset": "+02:00",
                    "F1ApiSupport": True,
                    "Session1Date": "2021-08-27T11:30:00",
                    "Session2Date": "2021-08-27T15:00:00",
                    "Session3Date": "2021-08-28T12:00:00",
                    "Session4Date": "2021-08-28T15:00:00",
                    "Session5Date": "2021-08-29T15:00:00"
                }
            }
        }
    }
}

validated = validate_schedule_payload(payload_with_metadata)

print(f"✓ Payload with metadata validated")
print(f"  Metadata fields: {list(validated['years']['2021']['metadata']['Belgian Grand Prix'].keys())}")

# Output:
# ✓ Payload with metadata validated
#   Metadata fields: ['RoundNumber', 'EventDate', 'Location', 'Country', ...]
```

##### Example 6: Error Recovery Pattern

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_and_validate_schedule(payload):
    """
    Attempt to validate a schedule payload with error recovery.

    Returns:
        tuple: (success: bool, validated_payload or error_message)
    """
    try:
        validated = validate_schedule_payload(payload)
        return True, validated
    except InvalidDataError as e:
        error_info = {
            "message": e.message,
            "reason": e.context.get("reason"),
            "context": e.context
        }
        return False, error_info

# Test with valid payload
valid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}

success, result = load_and_validate_schedule(valid_payload)
if success:
    print(f"✓ Validation succeeded")
    print(f"  Years available: {list(result['years'].keys())}")
else:
    print(f"✗ Validation failed: {result['reason']}")

# Test with invalid payload
invalid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {}  # Missing session list!
        }
    }
}

success, result = load_and_validate_schedule(invalid_payload)
if success:
    print(f"✓ Validation succeeded")
else:
    print(f"✗ Validation failed: {result['reason']}")

# Output:
# ✓ Validation succeeded
#   Years available: ['2021']
# ✗ Validation failed: Invalid session list for year=2021 event='Bahrain Grand Prix'
```

#### Common Validation Failures

Here's a comprehensive reference of common validation failures and how to fix them:

| Error Message                               | Cause                                                     | Solution                                         |
| ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `Schedule payload must be an object`        | Payload is not a dictionary                               | Ensure payload is a `dict`, not list/string/None |
| `Unsupported schedule schema version: X`    | Schema version is not 1                                   | Set `schema_version` to `1`                      |
| `Schedule payload missing 'years' object`   | Missing or invalid `years` key                            | Add `"years": {}` dictionary to payload          |
| `Invalid year key: 'X'`                     | Year key is not a numeric string                          | Use `"2021"` not `2021` or `"twenty-one"`        |
| `Year payload must be object for year=X`    | Year value is not a dictionary                            | Ensure each year maps to a dictionary            |
| `Invalid events list for year=X`            | `events` is missing, not a list, or contains non-strings  | Provide `"events": ["Event 1", "Event 2"]`       |
| `Invalid sessions map for year=X`           | `sessions` is missing or not a dictionary                 | Provide `"sessions": {}` dictionary              |
| `Invalid session list for year=X event='Y'` | Session list missing, not a list, or contains non-strings | Add `"Event Name": ["Session 1", "Session 2"]`   |

#### Performance Considerations

The validation function is optimized for production use:

* **Fast Execution**: Typical validation time is 0.3-0.8ms for multi-year schedules
* **No Data Copying**: Validates in-place without creating copies of the data structure
* **Early Exit**: Stops at the first validation failure, avoiding unnecessary checks
* **Minimal Allocations**: Uses efficient iteration patterns to minimize memory allocations

**Benchmark Results** (on modern hardware):

| Schedule Size | Events     | Sessions        | Validation Time |
| ------------- | ---------- | --------------- | --------------- |
| 1 year        | 22 events  | \~110 sessions  | \~0.2ms         |
| 5 years       | 110 events | \~550 sessions  | \~0.6ms         |
| 10 years      | 220 events | \~1100 sessions | \~1.1ms         |

<Tip>
  **Integration Tip**: When building custom schedule loaders or data pipelines, call `validate_schedule_payload()` immediately after constructing your payload and before any caching or API exposure. This ensures data quality at the earliest possible stage.
</Tip>

***

## Schedule Schema Specification

### Schema Structure

The internal schedule schema is designed to be event-centric, optimizing for the most common query patterns in Formula 1 data analysis. This structure differs from the raw f1schedule format (which uses a columnar layout) to provide better performance for event and session lookups.

#### Complete Schema Definition

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "schema_version": 1,
  "years": {
    "YYYY": {
      "events": [
        "Event Name 1",
        "Event Name 2",
        "..."
      ],
      "sessions": {
        "Event Name 1": [
          "Session 1",
          "Session 2",
          "..."
        ],
        "Event Name 2": [
          "Session 1",
          "Session 2",
          "..."
        ]
      },
      "metadata": {
        "Event Name 1": {
          "RoundNumber": 1,
          "EventDate": "YYYY-MM-DDTHH:MM:SS",
          "Location": "City/Circuit Name",
          "Country": "Country Name",
          "OfficialEventName": "FORMULA 1 OFFICIAL EVENT NAME YYYY",
          "EventFormat": "conventional|sprint|testing",
          "GmtOffset": "+HH:MM",
          "F1ApiSupport": true|false,
          "Session1Date": "YYYY-MM-DDTHH:MM:SS",
          "Session2Date": "YYYY-MM-DDTHH:MM:SS",
          "Session3Date": "YYYY-MM-DDTHH:MM:SS",
          "Session4Date": "YYYY-MM-DDTHH:MM:SS",
          "Session5Date": "YYYY-MM-DDTHH:MM:SS",
          "Session1DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session2DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session3DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session4DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session5DateUtc": "YYYY-MM-DDTHH:MM:SS"
        }
      }
    }
  }
}
```

#### Real-World Example: 2021 Belgian Grand Prix

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "schema_version": 1,
  "years": {
    "2021": {
      "events": [
        "Bahrain Grand Prix",
        "Emilia Romagna Grand Prix",
        "Portuguese Grand Prix",
        "Spanish Grand Prix",
        "Monaco Grand Prix",
        "Azerbaijan Grand Prix",
        "French Grand Prix",
        "Styrian Grand Prix",
        "Austrian Grand Prix",
        "British Grand Prix",
        "Hungarian Grand Prix",
        "Belgian Grand Prix",
        "Dutch Grand Prix",
        "Italian Grand Prix",
        "Russian Grand Prix",
        "Turkish Grand Prix",
        "United States Grand Prix",
        "Mexico City Grand Prix",
        "São Paulo Grand Prix",
        "Qatar Grand Prix",
        "Saudi Arabian Grand Prix",
        "Abu Dhabi Grand Prix"
      ],
      "sessions": {
        "Belgian Grand Prix": [
          "Practice 1",
          "Practice 2",
          "Practice 3",
          "Qualifying",
          "Race"
        ]
      },
      "metadata": {
        "Belgian Grand Prix": {
          "RoundNumber": 12,
          "EventDate": "2021-08-29T17:00:00",
          "Location": "Spa-Francorchamps",
          "Country": "Belgium",
          "OfficialEventName": "FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021",
          "EventFormat": "conventional",
          "GmtOffset": "+02:00",
          "F1ApiSupport": true,
          "Session1Date": "2021-08-27T11:30:00",
          "Session2Date": "2021-08-27T15:00:00",
          "Session3Date": "2021-08-28T12:00:00",
          "Session4Date": "2021-08-28T15:00:00",
          "Session5Date": "2021-08-29T15:00:00",
          "Session1DateUtc": "2021-08-27T09:30:00",
          "Session2DateUtc": "2021-08-27T13:00:00",
          "Session3DateUtc": "2021-08-28T10:00:00",
          "Session4DateUtc": "2021-08-28T13:00:00",
          "Session5DateUtc": "2021-08-29T13:00:00"
        }
      }
    }
  }
}
```

### Field Specifications

#### Top-Level Fields

| Field            | Type                     | Required | Description                                                                                                  |
| ---------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------ |
| `schema_version` | `int`                    | ✓        | Schema format version. Must be `1`. Future versions may introduce breaking changes.                          |
| `years`          | `dict[str, YearPayload]` | ✓        | Dictionary mapping year strings to year-specific schedule data. Keys must be numeric strings (e.g., "2021"). |

#### Year Payload Fields

| Field      | Type                       | Required | Description                                                                                                                                         |
| ---------- | -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `events`   | `list[str]`                | ✓        | Ordered list of event names for the year. Order typically follows championship calendar (race events by round number, then testing events by date). |
| `sessions` | `dict[str, list[str]]`     | ✓        | Maps each event name to its list of session names. Every event in `events` must have a corresponding entry.                                         |
| `metadata` | `dict[str, EventMetadata]` | ✗        | Optional metadata for each event. Not validated by `validate_schedule_payload()` but used by higher-level APIs.                                     |

#### Event Metadata Fields

| Field               | Type   | Required | Description                                                           | Example                                     |
| ------------------- | ------ | -------- | --------------------------------------------------------------------- | ------------------------------------------- |
| `RoundNumber`       | `int`  | ✗        | Championship round number. `0` for testing events.                    | `12`                                        |
| `EventDate`         | `str`  | ✗        | Main event date (typically race day) in ISO 8601 format (local time). | `"2021-08-29T17:00:00"`                     |
| `Location`          | `str`  | ✗        | Circuit or city name.                                                 | `"Spa-Francorchamps"`                       |
| `Country`           | `str`  | ✗        | Country where the event takes place.                                  | `"Belgium"`                                 |
| `OfficialEventName` | `str`  | ✗        | Full official FIA event name.                                         | `"FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021"` |
| `EventFormat`       | `str`  | ✗        | Event format type. Values: `"conventional"`, `"sprint"`, `"testing"`. | `"conventional"`                            |
| `GmtOffset`         | `str`  | ✗        | Local timezone offset from UTC.                                       | `"+02:00"`                                  |
| `F1ApiSupport`      | `bool` | ✗        | Whether the event is supported by F1's official API.                  | `true`                                      |
| `Session1Date`      | `str`  | ✗        | Date/time of first session in local time (ISO 8601).                  | `"2021-08-27T11:30:00"`                     |
| `Session2Date`      | `str`  | ✗        | Date/time of second session in local time.                            | `"2021-08-27T15:00:00"`                     |
| `Session3Date`      | `str`  | ✗        | Date/time of third session in local time.                             | `"2021-08-28T12:00:00"`                     |
| `Session4Date`      | `str`  | ✗        | Date/time of fourth session in local time.                            | `"2021-08-28T15:00:00"`                     |
| `Session5Date`      | `str`  | ✗        | Date/time of fifth session in local time.                             | `"2021-08-29T15:00:00"`                     |
| `Session1DateUtc`   | `str`  | ✗        | Date/time of first session in UTC (ISO 8601).                         | `"2021-08-27T09:30:00"`                     |
| `Session2DateUtc`   | `str`  | ✗        | Date/time of second session in UTC.                                   | `"2021-08-27T13:00:00"`                     |
| `Session3DateUtc`   | `str`  | ✗        | Date/time of third session in UTC.                                    | `"2021-08-28T10:00:00"`                     |
| `Session4DateUtc`   | `str`  | ✗        | Date/time of fourth session in UTC.                                   | `"2021-08-28T13:00:00"`                     |
| `Session5DateUtc`   | `str`  | ✗        | Date/time of fifth session in UTC.                                    | `"2021-08-29T13:00:00"`                     |

### Session Name Standards

Session names follow standardized conventions:

#### Standard Session Names

| Session Name | Abbreviation | Description                             | Typical Day |
| ------------ | ------------ | --------------------------------------- | ----------- |
| `Practice 1` | `FP1`        | First practice session                  | Friday      |
| `Practice 2` | `FP2`        | Second practice session                 | Friday      |
| `Practice 3` | `FP3`        | Third practice session                  | Saturday    |
| `Qualifying` | `Q`          | Qualifying session (sets grid for race) | Saturday    |
| `Race`       | `R`          | Main race                               | Sunday      |

#### Sprint Weekend Session Names

Sprint weekends have a different structure:

| Session Name        | Abbreviation | Description                                        | Typical Day |
| ------------------- | ------------ | -------------------------------------------------- | ----------- |
| `Practice 1`        | `FP1`        | First practice session                             | Friday      |
| `Qualifying`        | `Q`          | Qualifying (sets grid for Sunday race)             | Friday      |
| `Practice 2`        | `FP2`        | Second practice session (Sprint Shootout in 2023+) | Saturday    |
| `Sprint`            | `S`          | Sprint race (sets Sunday grid in 2021-2022)        | Saturday    |
| `Sprint Shootout`   | `SS`         | Sprint qualifying (sets Sprint grid, 2023+)        | Saturday    |
| `Sprint Qualifying` | `SQ`         | Sprint qualifying (2021-2022, same as Sprint)      | Saturday    |
| `Race`              | `R`          | Main race                                          | Sunday      |

<Info>
  **Sprint Format Evolution**: The sprint weekend format has evolved over the years:

  * **2021-2022**: Used "Sprint Qualifying" (later renamed to just "Sprint")
  * **2023+**: Introduced "Sprint Shootout" as a separate qualifying session for the Sprint race

  tif1 handles these variations automatically through backward compatibility logic in the `Event.get_session_name()` method.
</Info>

### Event Format Types

The `EventFormat` metadata field indicates the weekend structure:

| Format         | Description                     | Typical Sessions                      | Example Events                   |
| -------------- | ------------------------------- | ------------------------------------- | -------------------------------- |
| `conventional` | Standard race weekend           | FP1, FP2, FP3, Qualifying, Race       | Most Grand Prix events           |
| `sprint`       | Sprint race weekend             | FP1, Qualifying, FP2/SS, Sprint, Race | British GP 2021, Italian GP 2021 |
| `testing`      | Pre-season or in-season testing | Practice 1, Practice 2, Practice 3    | Pre-Season Test 2021             |

### Schema Version History

#### Version 1 (Current)

* **Introduced**: tif1 v0.1.0
* **Status**: Current and only supported version
* **Features**:
  * Event-centric structure
  * Support for conventional, sprint, and testing formats
  * Metadata with timezone information
  * Session date tracking (local and UTC)

#### Future Versions (Planned)

Future schema versions may include:

* **Version 2** (Tentative):
  * Circuit information (length, corners, DRS zones)
  * Weather forecast data
  * Tire compound allocations
  * Support for new session types (e.g., "Sprint Shootout")
  * Enhanced metadata for special events

<Warning>
  **Breaking Changes**: When new schema versions are introduced, they will be clearly documented with migration guides. The library will maintain backward compatibility where possible, but validation will require explicit version support.
</Warning>

***

## Validation Rules Reference

This section provides a comprehensive reference of all validation rules enforced by `validate_schedule_payload()`.

### Rule Categories

#### Category 1: Structural Validation

These rules ensure the payload has the correct overall structure:

| Rule ID | Check                    | Failure Condition            | Error Message                                    |
| ------- | ------------------------ | ---------------------------- | ------------------------------------------------ |
| **S1**  | Payload type             | Payload is not a `dict`      | `"Schedule payload must be an object"`           |
| **S2**  | Schema version presence  | `schema_version` key missing | `"Unsupported schedule schema version: None"`    |
| **S3**  | Schema version value     | `schema_version` is not `1`  | `"Unsupported schedule schema version: {value}"` |
| **S4**  | Years container presence | `years` key missing          | `"Schedule payload missing 'years' object"`      |
| **S5**  | Years container type     | `years` is not a `dict`      | `"Schedule payload missing 'years' object"`      |

#### Category 2: Year-Level Validation

These rules validate each year entry in the `years` dictionary:

| Rule ID | Check                 | Failure Condition                    | Error Message                                   |
| ------- | --------------------- | ------------------------------------ | ----------------------------------------------- |
| **Y1**  | Year key format       | Year key is not a string of digits   | `"Invalid year key: {key!r}"`                   |
| **Y2**  | Year payload type     | Year value is not a `dict`           | `"Year payload must be object for year={year}"` |
| **Y3**  | Events list presence  | `events` key missing or not a list   | `"Invalid events list for year={year}"`         |
| **Y4**  | Events list content   | Any event in list is not a string    | `"Invalid events list for year={year}"`         |
| **Y5**  | Events list non-empty | Any event string is empty            | `"Invalid events list for year={year}"`         |
| **Y6**  | Sessions map presence | `sessions` key missing or not a dict | `"Invalid sessions map for year={year}"`        |

#### Category 3: Session-Level Validation

These rules validate session mappings for each event:

| Rule ID | Check                  | Failure Condition                     | Error Message                                            |
| ------- | ---------------------- | ------------------------------------- | -------------------------------------------------------- |
| **E1**  | Session list presence  | Event has no entry in `sessions` dict | `"Invalid session list for year={year} event={event!r}"` |
| **E2**  | Session list type      | Session list is not a `list`          | `"Invalid session list for year={year} event={event!r}"` |
| **E3**  | Session list content   | Any session is not a string           | `"Invalid session list for year={year} event={event!r}"` |
| **E4**  | Session list non-empty | Any session string is empty           | `"Invalid session list for year={year} event={event!r}"` |

### Validation Order

The validation process follows this exact order:

```
1. Check payload is dict (S1)
2. Check schema_version exists and equals 1 (S2, S3)
3. Check years exists and is dict (S4, S5)
4. For each year in years:
   a. Check year key format (Y1)
   b. Check year payload is dict (Y2)
   c. Check events list exists and is valid (Y3, Y4, Y5)
   d. Check sessions dict exists (Y6)
   e. For each event in events list:
      i. Check session list exists and is valid (E1, E2, E3, E4)
```

<Info>
  **Early Exit Behavior**: Validation stops at the first rule violation. This means you'll only see one error at a time, even if multiple issues exist. Fix the reported error and re-validate to discover any additional issues.
</Info>

### Common Validation Scenarios

#### Scenario 1: Empty Payload

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {}

# Fails at: S2 (schema_version missing)
# Error: "Unsupported schedule schema version: None"
```

**Fix:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {}
}
# ✓ Valid (empty schedule)
```

#### Scenario 2: Wrong Data Type

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = [
    {"year": 2021, "events": ["Bahrain Grand Prix"]}
]

# Fails at: S1 (payload not a dict)
# Error: "Schedule payload must be an object"
```

**Fix:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}
# ✓ Valid
```

#### Scenario 3: Invalid Year Key

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        2021: {  # Integer instead of string
            "events": ["Bahrain Grand Prix"],
            "sessions": {"Bahrain Grand Prix": ["Race"]}
        }
    }
}

# Fails at: Y1 (year key not a string)
# Error: "Invalid year key: 2021"
```

**Fix:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {  # String key
            "events": ["Bahrain Grand Prix"],
            "sessions": {"Bahrain Grand Prix": ["Race"]}
        }
    }
}
# ✓ Valid
```

#### Scenario 4: Missing Session Mapping

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix"!
            }
        }
    }
}

# Fails at: E1 (session list missing for event)
# Error: "Invalid session list for year=2021 event='Belgian Grand Prix'"
```

**Fix:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
                "Belgian Grand Prix": ["Practice 1", "Qualifying", "Race"]  # Added
            }
        }
    }
}
# ✓ Valid
```

#### Scenario 5: Empty Session List

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": []  # Empty list
            }
        }
    }
}

# ✓ Valid (empty session lists are allowed)
```

<Note>
  **Empty Lists**: While empty event lists and empty session lists pass validation, they may cause issues in higher-level APIs that expect at least one session per event. The validation layer only checks structural correctness, not business logic constraints.
</Note>

#### Scenario 6: Invalid Session Type

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", None, "Race"]  # None not allowed
            }
        }
    }
}

# Fails at: E3 (session not a string)
# Error: "Invalid session list for year=2021 event='Bahrain Grand Prix'"
```

**Fix:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]  # All strings
            }
        }
    }
}
# ✓ Valid
```

### Validation Best Practices

#### 1. Validate Early

Always validate immediately after constructing or loading schedule data:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_custom_schedule(file_path):
    """Load and validate a custom schedule file."""
    import json

    with open(file_path) as f:
        payload = json.load(f)

    # Validate immediately after loading
    try:
        validated = validate_schedule_payload(payload)
        return validated
    except InvalidDataError as e:
        raise ValueError(f"Invalid schedule file: {e.message}") from e
```

#### 2. Provide Context in Errors

When wrapping validation, preserve error context:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_context(payload, source_name):
    """Validate with additional context."""
    try:
        return validate_schedule_payload(payload)
    except InvalidDataError as e:
        # Preserve original error context
        raise InvalidDataError(
            reason=f"Validation failed for {source_name}: {e.context.get('reason')}",
            source=source_name,
            original_error=e.message
        ) from e
```

#### 3. Build Payloads Incrementally

When constructing complex payloads, validate at each stage:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Start with minimal valid structure
payload = {
    "schema_version": 1,
    "years": {}
}

# Validate base structure
validate_schedule_payload(payload)  # ✓ Valid

# Add years incrementally
for year in [2021, 2022, 2023]:
    payload["years"][str(year)] = {
        "events": [],
        "sessions": {}
    }
    # Validate after each addition
    validate_schedule_payload(payload)  # ✓ Valid

# Add events and sessions
for year_str in payload["years"]:
    payload["years"][year_str]["events"] = ["Bahrain Grand Prix"]
    payload["years"][year_str]["sessions"] = {
        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
    }
    # Validate after modifications
    validate_schedule_payload(payload)  # ✓ Valid
```

#### 4. Handle Validation in Pipelines

For data processing pipelines, use validation as a quality gate:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def process_schedule_pipeline(raw_data):
    """Process schedule data with validation gates."""

    # Stage 1: Parse raw data
    parsed = parse_raw_schedule(raw_data)

    # Stage 2: Transform to internal format
    transformed = transform_to_internal_format(parsed)

    # Stage 3: Validate (quality gate)
    try:
        validated = validate_schedule_payload(transformed)
    except InvalidDataError as e:
        # Log validation failure and halt pipeline
        logger.error(f"Schedule validation failed: {e.message}")
        raise

    # Stage 4: Cache and return (only if validation passed)
    cache_schedule(validated)
    return validated
```

***

## Error Handling

### Exception Hierarchy

Schedule validation errors are part of tif1's exception hierarchy:

```
TIF1Error (base exception)
└── InvalidDataError
    └── Raised by validate_schedule_payload()
```

### InvalidDataError Structure

When validation fails, an `InvalidDataError` is raised with the following structure:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class InvalidDataError(TIF1Error):
    def __init__(self, reason: str | None = None, **context: Any):
        message = f"Invalid data: {reason}" if reason else "Invalid or corrupted data"
        super().__init__(message, reason=reason, **context)
```

**Attributes:**

* **`message`** (`str`): Human-readable error message
* **`context`** (`dict[str, Any]`): Structured error context containing:
  * **`reason`** (`str`): Detailed explanation of the validation failure
  * Additional context fields (varies by error type)

### Error Message Patterns

All validation error messages follow consistent patterns:

| Pattern                                                | Example                                   | Meaning                   |
| ------------------------------------------------------ | ----------------------------------------- | ------------------------- |
| `Schedule payload must be an object`                   | Exact message                             | Top-level structure error |
| `Unsupported schedule schema version: {version}`       | `...version: 2`                           | Schema version mismatch   |
| `Schedule payload missing 'years' object`              | Exact message                             | Missing years container   |
| `Invalid year key: {key!r}`                            | `...key: 'twenty-one'`                    | Year key format error     |
| `Year payload must be object for year={year}`          | `...year=2021`                            | Year value type error     |
| `Invalid events list for year={year}`                  | `...year=2021`                            | Events list error         |
| `Invalid sessions map for year={year}`                 | `...year=2021`                            | Sessions dict error       |
| `Invalid session list for year={year} event={event!r}` | `...year=2021 event='Belgian Grand Prix'` | Session list error        |

### Catching and Handling Errors

#### Basic Error Handling

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

try:
    validated = validate_schedule_payload(payload)
    print("✓ Validation succeeded")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")
```

#### Detailed Error Inspection

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

try:
    validated = validate_schedule_payload(payload)
except InvalidDataError as e:
    # Access all error information
    print(f"Error Type: {type(e).__name__}")
    print(f"Message: {e.message}")
    print(f"Reason: {e.context.get('reason')}")
    print(f"Full Context: {e.context}")

    # Check for specific error patterns
    if "schema version" in e.message.lower():
        print("→ Schema version issue detected")
    elif "year=" in e.message:
        print("→ Year-level validation issue")
    elif "event=" in e.message:
        print("→ Event-level validation issue")
```

#### Error Recovery Strategies

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_schedule_with_fallback(primary_payload, fallback_payload):
    """
    Attempt to load primary payload, fall back to secondary if validation fails.
    """
    try:
        return validate_schedule_payload(primary_payload)
    except InvalidDataError as e:
        logger.warning(f"Primary payload validation failed: {e.message}")
        logger.info("Attempting fallback payload...")

        try:
            return validate_schedule_payload(fallback_payload)
        except InvalidDataError as e2:
            logger.error(f"Fallback payload also failed: {e2.message}")
            raise ValueError("Both primary and fallback payloads are invalid") from e2
```

#### Logging Validation Errors

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import logging
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

logger = logging.getLogger(__name__)

def validate_and_log(payload, source_name):
    """Validate payload with comprehensive logging."""
    try:
        validated = validate_schedule_payload(payload)
        logger.info(f"✓ Validated schedule from {source_name}")
        logger.debug(f"  Years: {list(validated['years'].keys())}")
        return validated
    except InvalidDataError as e:
        logger.error(
            f"✗ Schedule validation failed for {source_name}",
            extra={
                "source": source_name,
                "error_message": e.message,
                "error_reason": e.context.get("reason"),
                "error_context": e.context
            }
        )
        raise
```

#### Custom Error Messages

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_custom_errors(payload, context_info):
    """Validate with application-specific error messages."""
    try:
        return validate_schedule_payload(payload)
    except InvalidDataError as e:
        # Transform technical error into user-friendly message
        user_message = _create_user_friendly_message(e, context_info)

        # Re-raise with custom message but preserve original error
        raise ValueError(user_message) from e

def _create_user_friendly_message(error, context):
    """Convert technical validation error to user-friendly message."""
    reason = error.context.get("reason", "")

    if "schema version" in reason.lower():
        return (
            f"The schedule file format is not supported. "
            f"Please ensure you're using a compatible schedule file. "
            f"(Technical: {reason})"
        )
    elif "year=" in reason:
        return (
            f"There's an issue with the schedule data for a specific year. "
            f"Please check the schedule file structure. "
            f"(Technical: {reason})"
        )
    elif "event=" in reason:
        return (
            f"There's an issue with session data for a specific event. "
            f"Please ensure all events have valid session lists. "
            f"(Technical: {reason})"
        )
    else:
        return (
            f"The schedule file structure is invalid. "
            f"Please check the file format. "
            f"(Technical: {reason})"
        )
```

### Debugging Validation Failures

#### Strategy 1: Incremental Validation

Build and validate the payload incrementally to isolate issues:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

# Start with minimal structure
payload = {"schema_version": 1, "years": {}}
validate_schedule_payload(payload)  # Should pass

# Add one year
payload["years"]["2021"] = {"events": [], "sessions": {}}
validate_schedule_payload(payload)  # Should pass

# Add one event
payload["years"]["2021"]["events"] = ["Bahrain Grand Prix"]
# Don't validate yet - we know this will fail (missing sessions)

# Add corresponding sessions
payload["years"]["2021"]["sessions"]["Bahrain Grand Prix"] = ["Race"]
validate_schedule_payload(payload)  # Should pass
```

#### Strategy 2: Payload Inspection

Inspect the payload structure before validation:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
from tif1.schedule_schema import validate_schedule_payload

def debug_validate(payload):
    """Validate with detailed debugging output."""
    print("=== Payload Structure ===")
    print(json.dumps(payload, indent=2))

    print("\n=== Validation Checks ===")

    # Check 1: Type
    print(f"1. Payload type: {type(payload).__name__}")
    if not isinstance(payload, dict):
        print("   ✗ FAIL: Must be dict")
        return
    print("   ✓ PASS")

    # Check 2: Schema version
    version = payload.get("schema_version")
    print(f"2. Schema version: {version}")
    if version != 1:
        print(f"   ✗ FAIL: Must be 1, got {version}")
        return
    print("   ✓ PASS")

    # Check 3: Years
    years = payload.get("years")
    print(f"3. Years type: {type(years).__name__}")
    if not isinstance(years, dict):
        print("   ✗ FAIL: Must be dict")
        return
    print(f"   ✓ PASS ({len(years)} years)")

    # Check 4: Each year
    for year_key, year_data in years.items():
        print(f"\n4. Year '{year_key}':")
        print(f"   - Key format: {'✓' if year_key.isdigit() else '✗'}")
        print(f"   - Value type: {type(year_data).__name__}")

        if isinstance(year_data, dict):
            events = year_data.get("events", [])
            sessions = year_data.get("sessions", {})
            print(f"   - Events: {len(events)} items")
            print(f"   - Sessions: {len(sessions)} mappings")

            # Check event-session consistency
            for event in events:
                has_sessions = event in sessions
                print(f"     • '{event}': {'✓' if has_sessions else '✗ MISSING SESSIONS'}")

    print("\n=== Running Validation ===")
    try:
        validate_schedule_payload(payload)
        print("✓ VALIDATION PASSED")
    except Exception as e:
        print(f"✗ VALIDATION FAILED: {e}")
```

#### Strategy 3: Diff Against Known-Good Payload

Compare a failing payload against a known-good example:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
from deepdiff import DeepDiff  # pip install deepdiff

def compare_payloads(test_payload, reference_payload):
    """Compare test payload structure against reference."""
    diff = DeepDiff(
        reference_payload,
        test_payload,
        ignore_order=True,
        view='tree'
    )

    if diff:
        print("=== Structural Differences ===")
        print(json.dumps(diff.to_dict(), indent=2))
    else:
        print("✓ Structures match")
```

### Common Error Scenarios and Solutions

| Scenario                  | Error                                       | Root Cause                             | Solution                                              |
| ------------------------- | ------------------------------------------- | -------------------------------------- | ----------------------------------------------------- |
| Loading JSON from file    | `Schedule payload must be an object`        | JSON file contains array at root       | Wrap array in `{"schema_version": 1, "years": {...}}` |
| Converting from DataFrame | `Invalid year key: '2021'`                  | Year column is numeric                 | Convert to string: `str(year)`                        |
| Merging schedules         | `Invalid session list for year=X event='Y'` | Event in one schedule missing sessions | Ensure all events have session mappings               |
| Custom data source        | `Unsupported schedule schema version: None` | Missing schema\_version field          | Add `"schema_version": 1` to payload                  |
| API response parsing      | `Year payload must be object for year=X`    | Year data is list instead of dict      | Transform API response structure                      |

<Tip>
  **Debugging Tip**: When validation fails, print the exact payload structure using `json.dumps(payload, indent=2)` to visually inspect the data hierarchy and identify structural issues.
</Tip>

***

## Integration with tif1

### How Validation Fits into tif1's Architecture

The schedule validation system is a foundational component that enables reliable operation of all schedule-related APIs in tif1. Here's how it integrates:

#### Internal Usage Flow

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Internal flow (simplified)
def _load_schedule_payload():
    """Internal function that loads and validates schedule data."""
    # 1. Load vendored schedule files
    vendored_years = _load_vendored_f1schedule_years()

    # 2. Construct payload
    payload = {
        "schema_version": 1,
        "years": vendored_years
    }

    # 3. Validate before caching
    validated = validate_schedule_payload(payload)

    # 4. Cache and return
    return validated
```

### Public APIs That Use Validation

All schedule-related public APIs depend on validated schedule data:

#### `get_events(year)`

Returns a list of event names for a given year.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

# Internally validates schedule before returning events
events = tif1.get_events(2021)
print(events)
# Output: ['Bahrain Grand Prix', 'Emilia Romagna Grand Prix', ...]
```

**Internal Flow:**

```
get_events(2021)
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Extract events for 2021
  → Return event list
```

#### `get_sessions(year, event)`

Returns a list of session names for a specific event.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

# Internally validates schedule before returning sessions
sessions = tif1.get_sessions(2021, "Belgian Grand Prix")
print(sessions)
# Output: ['Practice 1', 'Practice 2', 'Practice 3', 'Qualifying', 'Race']
```

**Internal Flow:**

```
get_sessions(2021, "Belgian Grand Prix")
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Extract sessions for event
  → Return session list
```

#### `get_event_schedule(year)`

Returns a pandas DataFrame with the complete event schedule for a year.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

# Internally validates schedule before building DataFrame
schedule = tif1.get_event_schedule(2021)
print(schedule[['RoundNumber', 'EventName', 'Location']])
```

**Internal Flow:**

```
get_event_schedule(2021)
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Build Event objects with metadata
  → Construct EventSchedule DataFrame
  → Return schedule
```

#### `get_event(year, identifier)`

Returns an Event object for a specific event.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

# Internally validates schedule before creating Event
event = tif1.get_event(2021, "Belgian Grand Prix")
print(f"{event['EventName']} at {event['Location']}")
# Output: Belgian Grand Prix at Spa-Francorchamps
```

**Internal Flow:**

```
get_event(2021, "Belgian Grand Prix")
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Find event by name (with fuzzy matching)
  → Create Event object with metadata
  → Return event
```

### Data Source Priority

tif1 uses a fallback system for schedule data:

```
┌─────────────────────────────────────────────────────────────┐
│ 1. Vendored JSON Files (Primary Source)                     │
│    Location: src/tif1/data/schedules/f1schedule/            │
│    Coverage: Years bundled with library (typically 2018+)   │
│    Validation: Runs once per Python session                 │
└─────────────────────────────────────────────────────────────┘
                          ↓ (if year not found)
┌─────────────────────────────────────────────────────────────┐
│ 2. CDN Fallback (Secondary Source)                          │
│    URL: jsdelivr.net/gh/theOehrly/f1schedule@master/        │
│    Coverage: All years available in f1schedule repo         │
│    Validation: Runs for each fetched year                   │
│    Caching: Cached after successful fetch                   │
└─────────────────────────────────────────────────────────────┘
```

Both sources go through the same validation pipeline, ensuring consistent data quality.

### Caching Strategy

Validation results are cached to minimize overhead:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from functools import lru_cache

@lru_cache(maxsize=1)
def _load_schedule_payload():
    """Load and validate schedule payload (cached)."""
    # Validation happens here, but only once per Python session
    vendored_years = _load_vendored_f1schedule_years()
    payload = {"schema_version": 1, "years": vendored_years}
    return validate_schedule_payload(payload)

@lru_cache(maxsize=16)
def _load_f1schedule_year_from_cdn(year):
    """Load and validate year from CDN (cached per year)."""
    # Validation happens here, but only once per year per session
    raw_data = fetch_from_cdn(year)
    converted = _convert_f1schedule_year(raw_data, year)
    # Validation is implicit in the conversion process
    return converted
```

**Cache Characteristics:**

* **Vendored Data**: Validated once per Python session, cached indefinitely
* **CDN Data**: Validated once per year per session, cached with LRU eviction (16 years max)
* **Performance Impact**: First call \~1ms, subsequent calls \~0.001ms (cache hit)

### Custom Schedule Data Integration

You can integrate custom schedule data with tif1's validation system:

#### Example: Loading Custom Schedule File

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_custom_schedule(file_path):
    """
    Load a custom schedule file and validate it.

    Args:
        file_path: Path to JSON file containing schedule data

    Returns:
        Validated schedule payload

    Raises:
        InvalidDataError: If schedule data is invalid
        FileNotFoundError: If file doesn't exist
        json.JSONDecodeError: If file is not valid JSON
    """
    # Load JSON file
    with open(file_path, 'r', encoding='utf-8') as f:
        payload = json.load(f)

    # Validate structure
    validated = validate_schedule_payload(payload)

    print(f"✓ Loaded and validated schedule from {file_path}")
    print(f"  Years: {list(validated['years'].keys())}")

    return validated

# Usage
try:
    custom_schedule = load_custom_schedule("my_schedule.json")
    # Use validated schedule data
    for year in custom_schedule['years']:
        events = custom_schedule['years'][year]['events']
        print(f"{year}: {len(events)} events")
except InvalidDataError as e:
    print(f"Invalid schedule file: {e.message}")
except FileNotFoundError:
    print("Schedule file not found")
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
```

#### Example: Building Schedule Programmatically

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload

def build_custom_schedule(years_data):
    """
    Build a schedule payload from structured data.

    Args:
        years_data: Dict mapping years to (events, sessions) tuples

    Returns:
        Validated schedule payload
    """
    payload = {
        "schema_version": 1,
        "years": {}
    }

    for year, (events, sessions) in years_data.items():
        payload["years"][str(year)] = {
            "events": events,
            "sessions": sessions
        }

    # Validate before returning
    return validate_schedule_payload(payload)

# Usage
years_data = {
    2021: (
        ["Bahrain Grand Prix", "Belgian Grand Prix"],
        {
            "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
            "Belgian Grand Prix": ["Practice 1", "Qualifying", "Race"]
        }
    ),
    2022: (
        ["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
        {
            "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
            "Saudi Arabian Grand Prix": ["Practice 1", "Qualifying", "Race"]
        }
    )
}

schedule = build_custom_schedule(years_data)
print(f"Built schedule with {len(schedule['years'])} years")
```

#### Example: Extending Vendored Schedule

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.events import _load_schedule_payload

def extend_schedule_with_custom_year(year, events, sessions):
    """
    Extend tif1's vendored schedule with a custom year.

    Args:
        year: Year to add (int)
        events: List of event names
        sessions: Dict mapping event names to session lists

    Returns:
        Extended and validated schedule payload
    """
    # Load existing schedule
    base_schedule = _load_schedule_payload()

    # Add custom year
    base_schedule["years"][str(year)] = {
        "events": events,
        "sessions": sessions
    }

    # Re-validate with new data
    return validate_schedule_payload(base_schedule)

# Usage
extended = extend_schedule_with_custom_year(
    year=2026,
    events=["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
    sessions={
        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
        "Saudi Arabian Grand Prix": ["Practice 1", "Qualifying", "Race"]
    }
)

print(f"Extended schedule now includes: {list(extended['years'].keys())}")
```

### Testing with Validation

When writing tests for schedule-related code, use validation to ensure test data quality:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import pytest
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

@pytest.fixture
def valid_schedule_payload():
    """Fixture providing a valid schedule payload for testing."""
    payload = {
        "schema_version": 1,
        "years": {
            "2021": {
                "events": ["Test Grand Prix"],
                "sessions": {
                    "Test Grand Prix": ["Practice 1", "Qualifying", "Race"]
                }
            }
        }
    }
    # Validate fixture data
    return validate_schedule_payload(payload)

def test_schedule_processing(valid_schedule_payload):
    """Test that uses validated schedule data."""
    # Test code here knows the payload is valid
    assert "2021" in valid_schedule_payload["years"]
    assert len(valid_schedule_payload["years"]["2021"]["events"]) == 1

def test_invalid_schedule_handling():
    """Test error handling for invalid schedules."""
    invalid_payload = {
        "schema_version": 1,
        "years": {
            "2021": {
                "events": ["Test Grand Prix"],
                "sessions": {}  # Missing sessions!
            }
        }
    }

    with pytest.raises(InvalidDataError) as exc_info:
        validate_schedule_payload(invalid_payload)

    assert "Invalid session list" in str(exc_info.value)
```

***

## Advanced Topics

### Raw f1schedule Format vs Internal Format

tif1 uses two different data formats internally:

#### Raw f1schedule Format (Columnar)

The vendored JSON files use a columnar format similar to pandas DataFrames:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "round_number": {
    "0": 0,
    "1": 1,
    "2": 2
  },
  "event_name": {
    "0": "Pre-Season Test",
    "1": "Bahrain Grand Prix",
    "2": "Emilia Romagna Grand Prix"
  },
  "country": {
    "0": "Bahrain",
    "1": "Bahrain",
    "2": "Italy"
  },
  "session1": {
    "0": "Practice 1",
    "1": "Practice 1",
    "2": "Practice 1"
  },
  "session1_date": {
    "0": "2021-03-12T10:00:00",
    "1": "2021-03-26T14:30:00",
    "2": "2021-04-16T11:00:00"
  }
  // ... more columns
}
```

**Characteristics:**

* **Structure**: Column-oriented (each field is a dictionary mapping indices to values)
* **Efficiency**: Compact storage, easy to convert to/from pandas DataFrames
* **Query Pattern**: Requires iteration to find specific events
* **Source**: f1schedule repository ([https://github.com/theOehrly/f1schedule](https://github.com/theOehrly/f1schedule))

#### Internal Format (Event-Centric)

After conversion and validation, tif1 uses an event-centric format:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "schema_version": 1,
  "years": {
    "2021": {
      "events": [
        "Pre-Season Test",
        "Bahrain Grand Prix",
        "Emilia Romagna Grand Prix"
      ],
      "sessions": {
        "Pre-Season Test": ["Practice 1", "Practice 2", "Practice 3"],
        "Bahrain Grand Prix": ["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        "Emilia Romagna Grand Prix": ["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"]
      },
      "metadata": {
        "Bahrain Grand Prix": {
          "RoundNumber": 1,
          "Country": "Bahrain",
          "Session1Date": "2021-03-26T14:30:00"
          // ... more metadata
        }
      }
    }
  }
}
```

**Characteristics:**

* **Structure**: Event-oriented (each event is a top-level entity)
* **Efficiency**: Fast event and session lookups (O(1) dictionary access)
* **Query Pattern**: Direct access by event name
* **Source**: Converted from raw format by `_convert_f1schedule_year()`

#### Conversion Process

The conversion from raw to internal format happens in `_convert_f1schedule_year()`:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def _convert_f1schedule_year(raw_year: dict, year: int) -> dict:
    """
    Convert raw f1schedule columnar format to internal event-centric format.

    Process:
    1. Extract column data (event_name, round_number, sessions, etc.)
    2. Iterate through indices to build event list
    3. Extract sessions for each event
    4. Build metadata dictionary
    5. Sort events (race events by round, testing by date)
    6. Return structured payload
    """
    # Implementation details in events.py
```

**Why Two Formats?**

* **Raw Format**: Optimized for storage and distribution (smaller file size, easier to maintain)
* **Internal Format**: Optimized for runtime queries (faster lookups, better API ergonomics)

### Performance Optimization Techniques

#### Technique 1: Lazy Loading

Schedule data is loaded only when first accessed:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from functools import lru_cache

@lru_cache(maxsize=1)
def _load_schedule_payload():
    """Load schedule only once per session."""
    # Expensive operations happen here
    vendored_years = _load_vendored_f1schedule_years()
    payload = {"schema_version": 1, "years": vendored_years}
    return validate_schedule_payload(payload)

# First call: loads and validates (~10-20ms)
schedule1 = _load_schedule_payload()

# Subsequent calls: returns cached result (~0.001ms)
schedule2 = _load_schedule_payload()
```

#### Technique 2: Immutable Caching

Event and session lists are cached as immutable tuples:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
@lru_cache(maxsize=16)
def _get_events_cached(year: int) -> tuple[str, ...]:
    """Cache events as immutable tuple for hashability."""
    return tuple(_build_events_for_year(year))

@lru_cache(maxsize=128)
def _get_sessions_cached(year: int, event: str) -> tuple[str, ...]:
    """Cache sessions as immutable tuple."""
    return tuple(_build_sessions_for_event(year, event))
```

**Benefits:**

* Tuples are hashable (can be used as cache keys)
* Immutability prevents accidental modification
* Smaller memory footprint than lists

#### Technique 3: Validation Short-Circuiting

Validation stops at the first error:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def validate_schedule_payload(payload):
    # Check 1: Type
    if not isinstance(payload, dict):
        raise InvalidDataError(...)  # Stop here

    # Check 2: Schema version
    if payload.get("schema_version") != 1:
        raise InvalidDataError(...)  # Stop here

    # ... more checks
```

**Benefits:**

* Faster failure for invalid data
* Reduces unnecessary computation
* Provides immediate feedback

#### Technique 4: In-Place Validation

Validation doesn't copy data:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def validate_schedule_payload(payload):
    # Validates structure without creating copies
    # Returns the same object if valid
    return payload
```

**Benefits:**

* Zero memory overhead
* Faster validation (no allocation/copying)
* Suitable for large schedules

### Extending the Validation System

#### Adding Custom Validation Rules

You can wrap the built-in validation with additional checks:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_schedule_with_business_rules(payload):
    """
    Validate schedule with additional business logic.
    """
    # First, run standard validation
    validated = validate_schedule_payload(payload)

    # Then, add custom rules
    for year_str, year_data in validated["years"].items():
        year = int(year_str)

        # Rule 1: Modern seasons must have at least 15 events
        if year >= 2020:
            events = year_data.get("events", [])
            if len(events) < 15:
                raise InvalidDataError(
                    reason=f"Year {year} has only {len(events)} events (minimum 15 required)"
                )

        # Rule 2: All events must have at least 3 sessions
        sessions = year_data.get("sessions", {})
        for event, session_list in sessions.items():
            if len(session_list) < 3:
                raise InvalidDataError(
                    reason=f"Event '{event}' in {year} has only {len(session_list)} sessions"
                )

        # Rule 3: Race session must exist for all events
        for event, session_list in sessions.items():
            if "Race" not in session_list:
                raise InvalidDataError(
                    reason=f"Event '{event}' in {year} missing 'Race' session"
                )

    return validated
```

#### Creating Validation Decorators

Wrap functions with validation:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from functools import wraps
from tif1.schedule_schema import validate_schedule_payload

def requires_valid_schedule(func):
    """Decorator that validates schedule payload before function execution."""
    @wraps(func)
    def wrapper(payload, *args, **kwargs):
        # Validate payload
        validated = validate_schedule_payload(payload)
        # Call original function with validated payload
        return func(validated, *args, **kwargs)
    return wrapper

@requires_valid_schedule
def process_schedule(payload):
    """Process schedule data (payload is guaranteed valid)."""
    for year in payload["years"]:
        print(f"Processing {year}...")
        # Process with confidence that structure is valid
```

#### Building Validation Pipelines

Chain multiple validation steps:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

class ScheduleValidationPipeline:
    """Pipeline for multi-stage schedule validation."""

    def __init__(self):
        self.validators = []

    def add_validator(self, validator_func, name=None):
        """Add a validation function to the pipeline."""
        self.validators.append((name or validator_func.__name__, validator_func))
        return self

    def validate(self, payload):
        """Run all validators in sequence."""
        current = payload

        for name, validator in self.validators:
            try:
                current = validator(current)
                print(f"✓ {name} passed")
            except InvalidDataError as e:
                print(f"✗ {name} failed: {e.message}")
                raise

        return current

# Usage
pipeline = ScheduleValidationPipeline()
pipeline.add_validator(validate_schedule_payload, "Schema Validation")
pipeline.add_validator(validate_schedule_with_business_rules, "Business Rules")

try:
    validated = pipeline.validate(my_payload)
    print("✓ All validations passed")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
```

### Schema Evolution and Migration

#### Handling Future Schema Versions

When schema version 2 is introduced, you might need migration logic:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def migrate_schedule_to_v1(payload):
    """
    Migrate schedule payload to schema version 1.

    Handles:
    - Version 0 (hypothetical legacy format)
    - Version 1 (current format)
    - Version 2 (future format - downgrades to v1)
    """
    version = payload.get("schema_version")

    if version == 1:
        # Already v1, just validate
        return validate_schedule_payload(payload)

    elif version == 0:
        # Migrate from v0 to v1
        migrated = {
            "schema_version": 1,
            "years": {}
        }

        # Migration logic here
        for year, data in payload.get("schedule", {}).items():
            migrated["years"][str(year)] = {
                "events": data.get("event_list", []),
                "sessions": data.get("session_map", {})
            }

        return validate_schedule_payload(migrated)

    elif version == 2:
        # Downgrade from v2 to v1 (strip v2-specific fields)
        downgraded = {
            "schema_version": 1,
            "years": {}
        }

        for year, data in payload.get("years", {}).items():
            downgraded["years"][year] = {
                "events": data.get("events", []),
                "sessions": data.get("sessions", {}),
                # Omit v2-specific fields like "circuits", "weather", etc.
            }

        return validate_schedule_payload(downgraded)

    else:
        raise InvalidDataError(
            reason=f"Cannot migrate from unsupported schema version: {version}"
        )
```

### Troubleshooting Guide

#### Issue: Validation Passes But API Fails

**Symptom**: `validate_schedule_payload()` succeeds, but `get_sessions()` returns empty list.

**Cause**: Metadata might be missing or malformed (not validated by schema validator).

**Solution**:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Check metadata separately
payload = validate_schedule_payload(payload)
year_data = payload["years"]["2021"]

if "metadata" in year_data:
    for event in year_data["events"]:
        if event not in year_data["metadata"]:
            print(f"Warning: Missing metadata for '{event}'")
```

#### Issue: Performance Degradation

**Symptom**: Validation becomes slow with large schedules.

**Cause**: Validation is O(n×m) where n=years, m=events per year.

**Solution**:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Profile validation
import time

start = time.perf_counter()
validated = validate_schedule_payload(payload)
elapsed = time.perf_counter() - start

print(f"Validation took {elapsed*1000:.2f}ms")

# If slow, check payload size
total_events = sum(
    len(year_data.get("events", []))
    for year_data in payload["years"].values()
)
print(f"Total events: {total_events}")
```

#### Issue: Cryptic Error Messages

**Symptom**: Error message doesn't clearly indicate the problem.

**Cause**: Complex nested structure makes errors hard to pinpoint.

**Solution**:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Use debug validation function (from earlier examples)
debug_validate(payload)

# Or add try-except around specific sections
try:
    validate_schedule_payload(payload)
except InvalidDataError as e:
    print(f"Error: {e.message}")
    print(f"Context: {e.context}")

    # Inspect payload at failure point
    import json
    print("\nPayload structure:")
    print(json.dumps(payload, indent=2, default=str))
```

***

## Complete Working Examples

### Example 1: Basic Schedule Validation

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Basic example: Validate a simple schedule payload.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# Create a minimal valid schedule
schedule = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": [
                "Bahrain Grand Prix",
                "Belgian Grand Prix",
                "Abu Dhabi Grand Prix"
            ],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Abu Dhabi Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

try:
    validated = validate_schedule_payload(schedule)
    print("✓ Schedule validation successful!")
    print(f"  Years: {list(validated['years'].keys())}")
    print(f"  Events in 2021: {len(validated['years']['2021']['events'])}")

    for event in validated['years']['2021']['events']:
        sessions = validated['years']['2021']['sessions'][event]
        print(f"  • {event}: {len(sessions)} sessions")

except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")
```

### Example 2: Loading and Validating from JSON File

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Load schedule data from a JSON file and validate it.
"""
import json
from pathlib import Path
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_schedule_file(file_path: str | Path) -> dict:
    """
    Load and validate a schedule JSON file.

    Args:
        file_path: Path to the JSON file

    Returns:
        Validated schedule payload

    Raises:
        FileNotFoundError: If file doesn't exist
        json.JSONDecodeError: If file contains invalid JSON
        InvalidDataError: If schedule structure is invalid
    """
    file_path = Path(file_path)

    # Check file exists
    if not file_path.exists():
        raise FileNotFoundError(f"Schedule file not found: {file_path}")

    # Load JSON
    print(f"Loading schedule from {file_path}...")
    with open(file_path, 'r', encoding='utf-8') as f:
        payload = json.load(f)

    # Validate
    print("Validating schedule structure...")
    validated = validate_schedule_payload(payload)

    # Report success
    years = list(validated['years'].keys())
    total_events = sum(
        len(validated['years'][year]['events'])
        for year in years
    )

    print(f"✓ Schedule loaded successfully")
    print(f"  Years: {', '.join(years)}")
    print(f"  Total events: {total_events}")

    return validated

# Usage
try:
    schedule = load_schedule_file("my_schedule.json")

    # Use the validated schedule
    for year in schedule['years']:
        events = schedule['years'][year]['events']
        print(f"\n{year} Season ({len(events)} events):")
        for event in events[:3]:  # Show first 3
            print(f"  • {event}")
        if len(events) > 3:
            print(f"  ... and {len(events) - 3} more")

except FileNotFoundError as e:
    print(f"✗ Error: {e}")
except json.JSONDecodeError as e:
    print(f"✗ Invalid JSON: {e}")
except InvalidDataError as e:
    print(f"✗ Invalid schedule structure: {e.message}")
```

### Example 3: Building Schedule Programmatically

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Build a schedule payload programmatically with validation.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

class ScheduleBuilder:
    """Helper class for building valid schedule payloads."""

    def __init__(self):
        self.payload = {
            "schema_version": 1,
            "years": {}
        }

    def add_year(self, year: int):
        """Add a year to the schedule."""
        self.payload["years"][str(year)] = {
            "events": [],
            "sessions": {},
            "metadata": {}
        }
        return self

    def add_event(self, year: int, event_name: str, sessions: list[str],
                  metadata: dict | None = None):
        """Add an event to a specific year."""
        year_str = str(year)

        if year_str not in self.payload["years"]:
            self.add_year(year)

        # Add to events list
        self.payload["years"][year_str]["events"].append(event_name)

        # Add sessions
        self.payload["years"][year_str]["sessions"][event_name] = sessions

        # Add metadata if provided
        if metadata:
            self.payload["years"][year_str]["metadata"][event_name] = metadata

        return self

    def build(self):
        """Build and validate the schedule."""
        return validate_schedule_payload(self.payload)

# Usage
try:
    builder = ScheduleBuilder()

    # Add 2021 season
    builder.add_event(
        year=2021,
        event_name="Bahrain Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 1,
            "Location": "Sakhir",
            "Country": "Bahrain"
        }
    )

    builder.add_event(
        year=2021,
        event_name="Belgian Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 12,
            "Location": "Spa-Francorchamps",
            "Country": "Belgium"
        }
    )

    # Add 2022 season
    builder.add_event(
        year=2022,
        event_name="Bahrain Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 1,
            "Location": "Sakhir",
            "Country": "Bahrain"
        }
    )

    # Build and validate
    schedule = builder.build()

    print("✓ Schedule built successfully!")
    for year in schedule['years']:
        events = schedule['years'][year]['events']
        print(f"  {year}: {len(events)} events")

except InvalidDataError as e:
    print(f"✗ Failed to build schedule: {e.message}")
```

### Example 4: Validating Sprint Weekend Format

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Example showing validation of sprint weekend schedules.
"""
from tif1.schedule_schema import validate_schedule_payload

# 2021 Sprint Weekend (British GP)
sprint_2021 = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["British Grand Prix"],
            "sessions": {
                "British Grand Prix": [
                    "Practice 1",      # Friday
                    "Qualifying",      # Friday (for Sunday grid)
                    "Practice 2",      # Saturday
                    "Sprint",          # Saturday (sets Sunday grid)
                    "Race"             # Sunday
                ]
            },
            "metadata": {
                "British Grand Prix": {
                    "RoundNumber": 10,
                    "EventFormat": "sprint",
                    "Location": "Silverstone",
                    "Country": "Great Britain"
                }
            }
        }
    }
}

# 2023 Sprint Weekend (with Sprint Shootout)
sprint_2023 = {
    "schema_version": 1,
    "years": {
        "2023": {
            "events": ["Azerbaijan Grand Prix"],
            "sessions": {
                "Azerbaijan Grand Prix": [
                    "Practice 1",         # Friday
                    "Qualifying",         # Friday (for Sunday grid)
                    "Sprint Shootout",    # Saturday (for Sprint grid)
                    "Sprint",             # Saturday
                    "Race"                # Sunday
                ]
            },
            "metadata": {
                "Azerbaijan Grand Prix": {
                    "RoundNumber": 4,
                    "EventFormat": "sprint",
                    "Location": "Baku",
                    "Country": "Azerbaijan"
                }
            }
        }
    }
}

# Validate both formats
for name, schedule in [("2021 Sprint", sprint_2021), ("2023 Sprint", sprint_2023)]:
    try:
        validated = validate_schedule_payload(schedule)
        year = list(validated['years'].keys())[0]
        event = validated['years'][year]['events'][0]
        sessions = validated['years'][year]['sessions'][event]

        print(f"✓ {name} format validated")
        print(f"  Event: {event}")
        print(f"  Sessions: {', '.join(sessions)}")
        print()
    except Exception as e:
        print(f"✗ {name} validation failed: {e}")
```

### Example 5: Integration with tif1 APIs

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Complete example showing how validation integrates with tif1's public APIs.
"""
import tif1
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# The validation happens automatically when using tif1 APIs
print("=== Using tif1 Public APIs ===\n")

# Get events for 2021 (internally validates schedule)
events = tif1.get_events(2021)
print(f"2021 Events ({len(events)} total):")
for event in events[:5]:
    print(f"  • {event}")
print(f"  ... and {len(events) - 5} more\n")

# Get sessions for a specific event (internally validates schedule)
sessions = tif1.get_sessions(2021, "Belgian Grand Prix")
print(f"Belgian Grand Prix 2021 Sessions:")
for session in sessions:
    print(f"  • {session}")
print()

# Get full event schedule (internally validates schedule)
schedule = tif1.get_event_schedule(2021)
print(f"Full 2021 Schedule:")
print(f"  Type: {type(schedule).__name__}")
print(f"  Shape: {schedule.shape}")
print(f"  Columns: {list(schedule.columns[:5])}...")
print()

# Get specific event (internally validates schedule)
event = tif1.get_event(2021, "Belgian Grand Prix")
print(f"Belgian Grand Prix Event:")
print(f"  Round: {event['RoundNumber']}")
print(f"  Location: {event['Location']}")
print(f"  Country: {event['Country']}")
print(f"  Format: {event['EventFormat']}")
print()

# Manual validation (for custom data)
print("=== Manual Validation ===\n")

custom_schedule = {
    "schema_version": 1,
    "years": {
        "2026": {
            "events": ["Test Grand Prix"],
            "sessions": {
                "Test Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}

try:
    validated = validate_schedule_payload(custom_schedule)
    print("✓ Custom schedule validated successfully")
    print(f"  Years: {list(validated['years'].keys())}")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
```

### Example 6: Error Handling and Recovery

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
"""
Comprehensive error handling example.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_detailed_error_handling(payload):
    """
    Validate schedule with comprehensive error handling and reporting.
    """
    try:
        validated = validate_schedule_payload(payload)

        # Success - report details
        years = list(validated['years'].keys())
        total_events = sum(
            len(validated['years'][year]['events'])
            for year in years
        )
        total_sessions = sum(
            len(session_list)
            for year in years
            for session_list in validated['years'][year]['sessions'].values()
        )

        print("✓ Validation successful!")
        print(f"  Years: {len(years)} ({', '.join(years)})")
        print(f"  Total events: {total_events}")
        print(f"  Total sessions: {total_sessions}")

        return validated

    except InvalidDataError as e:
        # Detailed error analysis
        reason = e.context.get('reason', '')

        print("✗ Validation failed!")
        print(f"  Error: {e.message}")
        print(f"  Reason: {reason}")

        # Provide specific guidance based on error type
        if "schema version" in reason.lower():
            print("\n  💡 Fix: Set 'schema_version' to 1")
            print("     Example: {'schema_version': 1, 'years': {...}}")

        elif "year key" in reason.lower():
            print("\n  💡 Fix: Use numeric string for year keys")
            print("     Example: '2021' not 2021 or 'twenty-one'")

        elif "events list" in reason.lower():
            print("\n  💡 Fix: Ensure 'events' is a list of non-empty strings")
            print("     Example: 'events': ['Bahrain Grand Prix', 'Belgian Grand Prix']")

        elif "session list" in reason.lower():
            print("\n  💡 Fix: Ensure every event has a session list")
            print("     Example: 'sessions': {'Event Name': ['Practice 1', 'Race']}")

        else:
            print("\n  💡 Check the payload structure against the schema")

        return None

# Test cases
test_cases = [
    # Valid payload
    {
        "name": "Valid Schedule",
        "payload": {
            "schema_version": 1,
            "years": {
                "2021": {
                    "events": ["Bahrain Grand Prix"],
                    "sessions": {
                        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                    }
                }
            }
        }
    },
    # Invalid: wrong schema version
    {
        "name": "Wrong Schema Version",
        "payload": {
            "schema_version": 2,
            "years": {}
        }
    },
    # Invalid: missing sessions
    {
        "name": "Missing Sessions",
        "payload": {
            "schema_version": 1,
            "years": {
                "2021": {
                    "events": ["Bahrain Grand Prix"],
                    "sessions": {}
                }
            }
        }
    }
]

# Run test cases
for test in test_cases:
    print(f"\n{'='*60}")
    print(f"Test: {test['name']}")
    print('='*60)
    validate_with_detailed_error_handling(test['payload'])
```

***

## Summary

The `schedule_schema` module provides the foundational validation layer for tif1's schedule data system. Key takeaways:

### Core Concepts

* **Single Validation Function**: `validate_schedule_payload()` is the only public API
* **Schema Version 1**: Currently the only supported version
* **Event-Centric Structure**: Internal format optimized for fast event/session lookups
* **Fail-Fast Validation**: Stops at first error with detailed error messages
* **Performance Optimized**: Typically \<1ms validation time, cached results

### When to Use

* **Automatic**: All tif1 schedule APIs use validation internally
* **Manual**: When working with custom schedule data or building data pipelines
* **Testing**: To ensure test fixtures have valid structure
* **Debugging**: To diagnose schedule data issues

### Best Practices

1. **Validate Early**: Run validation immediately after loading or constructing schedule data
2. **Handle Errors**: Always catch `InvalidDataError` and provide user-friendly messages
3. **Preserve Context**: When wrapping validation, preserve error context for debugging
4. **Build Incrementally**: For complex payloads, validate at each construction stage
5. **Use Caching**: Leverage tif1's built-in caching to minimize validation overhead

### Common Patterns

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Pattern 1: Load and validate from file
with open("schedule.json") as f:
    payload = json.load(f)
validated = validate_schedule_payload(payload)

# Pattern 2: Build programmatically
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Event 1"],
            "sessions": {"Event 1": ["Session 1"]}
        }
    }
}
validated = validate_schedule_payload(payload)

# Pattern 3: Error handling
try:
    validated = validate_schedule_payload(payload)
except InvalidDataError as e:
    print(f"Validation failed: {e.context.get('reason')}")
```

### Related Documentation

For more information on working with schedule data in tif1:

<CardGroup cols={2}>
  <Card title="Events & Schedule API" href="/api-reference/events" icon="calendar">
    High-level APIs for accessing event and session information
  </Card>

  <Card title="Core Session API" href="/api-reference/core" icon="database">
    Loading and working with session data
  </Card>

  <Card title="Exception Handling" href="/api-reference/exceptions" icon="triangle-exclamation">
    Complete exception hierarchy and error handling patterns
  </Card>

  <Card title="Data Flow Concepts" href="/concepts/data-flow" icon="diagram-project">
    Understanding tif1's data pipeline architecture
  </Card>
</CardGroup>

### Quick Reference

#### Validation Rules Summary

| Level         | What's Checked                                         | Error Pattern                                        |
| ------------- | ------------------------------------------------------ | ---------------------------------------------------- |
| **Top-Level** | Payload is dict, has schema\_version=1, has years dict | `Schedule payload must be an object`                 |
| **Year**      | Year keys are numeric strings, values are dicts        | `Invalid year key: {key}`                            |
| **Events**    | Each year has events list with non-empty strings       | `Invalid events list for year={year}`                |
| **Sessions**  | Each year has sessions dict                            | `Invalid sessions map for year={year}`               |
| **Mapping**   | Each event has session list with non-empty strings     | `Invalid session list for year={year} event={event}` |

#### Performance Benchmarks

| Operation             | Time      | Notes                         |
| --------------------- | --------- | ----------------------------- |
| First validation      | 0.3-0.8ms | Includes parsing and checking |
| Cached access         | \~0.001ms | LRU cache hit                 |
| Multi-year (5 years)  | \~0.6ms   | \~110 events, \~550 sessions  |
| Multi-year (10 years) | \~1.1ms   | \~220 events, \~1100 sessions |

#### Schema Structure Quick Reference

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "schema_version": 1,                    # Required: Must be 1
  "years": {                              # Required: Dict of years
    "YYYY": {                             # Year as string
      "events": ["Event 1", "Event 2"],   # Required: List of event names
      "sessions": {                       # Required: Event → sessions mapping
        "Event 1": ["Session 1", "..."],  # Required for each event
        "Event 2": ["Session 1", "..."]
      },
      "metadata": {                       # Optional: Event metadata
        "Event 1": {                      # Optional per event
          "RoundNumber": 1,
          "Location": "...",
          # ... more fields
        }
      }
    }
  }
}
```

***

## Additional Resources

### External Links

* **f1schedule Repository**: [github.com/theOehrly/f1schedule](https://github.com/theOehrly/f1schedule) - Source of raw schedule data
* **tif1 GitHub**: [github.com/TracingInsights/tif1](https://github.com/TracingInsights/tif1) - Main repository
* **Issue Tracker**: Report schedule validation issues on GitHub

### Community

* **Discussions**: Ask questions about schedule validation on GitHub Discussions
* **Contributing**: Contributions to improve validation are welcome

### Version History

| Version     | Changes                                             |
| ----------- | --------------------------------------------------- |
| **v0.1.0**  | Initial release with schema version 1 support       |
| **Current** | Schema version 1 remains the only supported version |

<Info>
  **Stay Updated**: Watch the tif1 repository for announcements about new schema versions or validation enhancements.
</Info>

***

*Last updated: April 2026*
