# Todo Serialization Format and Mapping Logic in Lifetrace: A Complete Guide

> Explore Lifetrace's todo serialization format and mapping logic. Learn how SQLAlchemy models convert to flat Python dictionaries and match the TodoResponse schema for JSON serialization.

- Repository: [FreeU-group/lifetrace](https://github.com/freeu-group/lifetrace)
- Tags: deep-dive
- Published: 2026-03-02

---

**The Lifetrace backend converts SQLAlchemy Todo models into flat Python dictionaries using `TodoIcalMixin._todo_to_dict`, applying field-specific fallback logic and normalization helpers to match the `TodoResponse` schema before JSON serialization.**

The freeu-group/lifetrace repository provides a robust task management backend that bridges database storage with API consumption and iCalendar export. Understanding the **todo serialization format** is essential for developers integrating with the Lifetrace API or extending its calendar functionality. When a Todo entity is retrieved from the database, it undergoes a precise transformation process defined in the storage layer that maps SQLAlchemy model attributes to a standardized dictionary structure.

## Core Serialization Architecture

### The _todo_to_dict Method

The conversion logic resides in [`lifetrace/storage/todo_manager_ical.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/todo_manager_ical.py) within the `TodoIcalMixin` class. The `_todo_to_dict` method accepts an active SQLAlchemy session and a Todo model instance, returning a plain Python dictionary that aligns exactly with the `TodoResponse` schema defined in [`lifetrace/schemas/todo.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/schemas/todo.py).

This flat dictionary serves as the intermediate format for both JSON API responses and iCalendar component generation, ensuring consistent data representation across all output channels.

## Field Mapping Logic and Fallbacks

The mapping logic handles over 30 fields with intelligent fallbacks to ensure data integrity and backward compatibility:

- **id**: Required field mapping directly from `todo.id`; raises `ValueError` when missing.
- **uid**: Maps from `todo.uid`; may be `None`.
- **name**: Direct mapping from `todo.name`.
- **summary**: Uses `getattr(todo, "summary", None) or todo.name` to guarantee a non-empty string by falling back to the `name` field.
- **due**: Maps from `todo.due` with fallback to the legacy `todo.deadline` field.
- **dtstart/dtend**: Maps from `todo.dtstart` and `todo.dtend`, falling back to `start_time` and `end_time` respectively.
- **time_zone**: Maps from `todo.time_zone`, with `tzid` as an alternative source.
- **is_all_day**: Boolean field that defaults to `False` when the database column is `None`.
- **percent_complete**: Defaults to `0` when the database value is `None`.
- **order**: Defaults to `0` when not explicitly set.
- **item_type**: Stored as an upper-case iCalendar component (e.g., `VTODO`).
- **ical_status**: Maps from `todo.ical_status` or derives from `todo.status` via `_to_ical_status`.
- **dtstamp, created, last_modified**: Various timestamp columns with fallbacks to `updated_at`.
- **source_type, source_key, source_date**: Direct attribute mappings for optional metadata.

Relational data is hydrated separately via abstract methods:
- **tags**: Retrieved via `_get_todo_tags(session, todo_id)`, returning a list of strings.
- **attachments**: Retrieved via `_get_todo_attachments(session, todo_id)`, returning a list of dictionaries matching `TodoAttachmentResponse`.
- **related_activities**: Processed through `_safe_int_list` to guarantee a list of integers.

## Data Normalization Helpers

Specialized utilities in [`lifetrace/storage/todo_manager_utils.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/todo_manager_utils.py) ensure consistent data formats before serialization.

### Status Mapping with _to_ical_status

The `_to_ical_status` function maps internal status values to iCalendar-compliant strings. According to the source code, `active` maps to `NEEDS-ACTION`, `completed` maps to `COMPLETED`, and `canceled` maps to `CANCELLED`.

### Reminder Offset Normalization

The `_normalize_reminder_offsets` helper parses raw JSON strings into clean, sorted lists of positive integers. For example, the input `'[30, "15", -5, "bad"]'` is sanitized to `[15, 30]`, with negative values, duplicates, and invalid entries removed. The test suite in [`tests/test_todo_serialization.py`](https://github.com/freeu-group/lifetrace/blob/main/tests/test_todo_serialization.py) confirms that `None` values become empty lists.

### Safe Integer List Conversion

The `_safe_int_list` utility safely converts potentially null or JSON values into lists of integers, used specifically for the `related_activities` field to ensure type safety.

## Working with Todo Serialization

### Converting a Todo Instance

```python

# Assuming manager is a concrete subclass of TodoIcalMixin

# and session is an active SQLAlchemy session

from lifetrace.storage.models import Todo

todo = session.query(Todo).get(42)
data = manager._todo_to_dict(session, todo)

# data is now a dict matching TodoResponse schema

```

### Expected JSON Output Structure

```json
{
  "id": 42,
  "uid": "2023-10-12T09:00:00Z",
  "name": "Buy groceries",
  "summary": "Buy groceries",
  "description": "Milk, eggs, bread",
  "is_all_day": false,
  "status": "active",
  "priority": "none",
  "reminder_offsets": [10, 30],
  "tags": ["shopping", "errands"],
  "attachments": [],
  "related_activities": [7, 12],
  "created_at": "2023-10-10T12:00:00Z",
  "updated_at": "2023-10-11T08:45:00Z"
}

```

### Using Normalization Helpers Directly

```python
from lifetrace.storage.todo_manager_utils import _normalize_reminder_offsets
from lifetrace.storage.todo_manager_ical import _to_ical_status

# Clean malformed reminder offsets

raw_offsets = '[30, "15", -5, "bad"]'
clean_offsets = _normalize_reminder_offsets(raw_offsets)

# Result: [15, 30]

# Map status to iCalendar format

ical_status = _to_ical_status("completed")

# Result: "COMPLETED"

```

## Summary

- The **todo serialization format** in Lifetrace is a flat Python dictionary matching the `TodoResponse` Pydantic schema defined in [`lifetrace/schemas/todo.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/schemas/todo.py).
- **Core logic** resides in [`lifetrace/storage/todo_manager_ical.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/todo_manager_ical.py), specifically within `TodoIcalMixin._todo_to_dict`.
- **Field fallbacks** ensure data completeness, such as `summary` falling back to `name`, and `due` falling back to `deadline`.
- **Normalization helpers** enforce data quality by parsing reminder offsets, mapping status values to iCalendar format, and safely converting integer lists.
- **Relational data** (tags and attachments) is loaded via abstract methods that must be implemented by concrete manager subclasses.

## Frequently Asked Questions

### Where is the main todo serialization logic implemented?

The primary serialization logic is implemented in [`lifetrace/storage/todo_manager_ical.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/todo_manager_ical.py) within the `TodoIcalMixin` class. The `_todo_to_dict` method performs the conversion from SQLAlchemy model to dictionary format that matches the `TodoResponse` schema.

### How does Lifetrace handle missing summary fields?

When the `summary` attribute is null or missing, the serializer falls back to the `name` field using the expression `getattr(todo, "summary", None) or todo.name`. This ensures the API always returns a non-empty string for the summary field.

### What format are reminder offsets stored in after serialization?

Reminder offsets are normalized into a sorted list of positive integers. The `_normalize_reminder_offsets` helper parses JSON strings, filters out negative values and duplicates, and converts valid entries to integers, returning an empty list if the input is `None`.

### How is the Todo status mapped to iCalendar format?

The `_to_ical_status` function maps internal status values to iCalendar-compliant strings: `active` becomes `NEEDS-ACTION`, `completed` becomes `COMPLETED`, and `canceled` becomes `CANCELLED`. This mapping ensures compatibility with standard calendar applications.