From 1a707ff179981da7974f35bd0c58663a5c74e9bd Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sun, 22 Feb 2026 01:50:52 +0800 Subject: [PATCH] Fix weekly recurrence crash: null fields in serialized rule model_dump() includes None values for optional RecurrenceRule fields. When serialized to JSON, these become explicit nulls (e.g. "weekday": null). The recurrence service then does int(None) which raises TypeError. Fix: strip None values when serializing rule to JSON, and add defensive None handling in recurrence service for all rule.get() calls. Co-Authored-By: Claude Opus 4.6 --- backend/app/routers/events.py | 5 +++-- backend/app/services/recurrence.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/app/routers/events.py b/backend/app/routers/events.py index 6b2b022..a3a655c 100644 --- a/backend/app/routers/events.py +++ b/backend/app/routers/events.py @@ -192,8 +192,9 @@ async def create_event( data["calendar_id"] = await _get_default_calendar_id(db) # Serialize RecurrenceRule object to JSON string for DB storage + # Exclude None values so defaults in recurrence service work correctly rule_obj = data.pop("recurrence_rule", None) - rule_json: Optional[str] = json.dumps(rule_obj) if rule_obj else None + rule_json: Optional[str] = json.dumps({k: v for k, v in rule_obj.items() if v is not None}) if rule_obj else None if rule_json: # Parent template: is_recurring=True, no parent_event_id @@ -282,7 +283,7 @@ async def update_event( # Serialize RecurrenceRule → JSON string if present in update payload rule_obj = update_data.pop("recurrence_rule", None) if rule_obj is not None: - update_data["recurrence_rule"] = json.dumps(rule_obj) if rule_obj else None + update_data["recurrence_rule"] = json.dumps({k: v for k, v in rule_obj.items() if v is not None}) if rule_obj else None start = update_data.get("start_datetime", event.start_datetime) end_dt = update_data.get("end_datetime", event.end_datetime) diff --git a/backend/app/services/recurrence.py b/backend/app/services/recurrence.py index 4fcddb1..cb0406d 100644 --- a/backend/app/services/recurrence.py +++ b/backend/app/services/recurrence.py @@ -85,7 +85,7 @@ def generate_occurrences( ) if rule_type == "every_n_days": - interval: int = int(rule.get("interval", 1)) + interval: int = int(rule.get("interval") or 1) if interval < 1: interval = 1 current = parent_start + timedelta(days=interval) @@ -94,7 +94,7 @@ def generate_occurrences( current += timedelta(days=interval) elif rule_type == "weekly": - weekday: int = int(rule.get("weekday", parent_start.weekday())) + weekday: int = int(rule.get("weekday") if rule.get("weekday") is not None else parent_start.weekday()) # Start from the week after the parent days_ahead = (weekday - parent_start.weekday()) % 7 if days_ahead == 0: @@ -105,8 +105,8 @@ def generate_occurrences( current += timedelta(weeks=1) elif rule_type == "monthly_nth_weekday": - week: int = int(rule.get("week", 1)) - weekday = int(rule.get("weekday", parent_start.weekday())) + week: int = int(rule.get("week") or 1) + weekday = int(rule.get("weekday") if rule.get("weekday") is not None else parent_start.weekday()) # Advance month by month year, month = parent_start.year, parent_start.month # Move to the next month from the parent @@ -139,7 +139,7 @@ def generate_occurrences( break elif rule_type == "monthly_date": - day: int = int(rule.get("day", parent_start.day)) + day: int = int(rule.get("day") or parent_start.day) year, month = parent_start.year, parent_start.month month += 1 if month > 12: