{"record":{"id":"34ff17069b5347e8","repo":"MemPalace/mempalace","slug":"field-name-must-be-an-iso-date-string","errorCode":null,"errorMessage":"{field_name} must be an ISO date string","messagePattern":"(.+?) must be an ISO date string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/date_window.py","lineNumber":49,"sourceCode":"    comparison against drawer ``filed_at`` values, which are stored as naive\n    local ISO strings (``datetime.now().isoformat()``). Any timezone offset on\n    the input is dropped so an aware bound never raises a ``TypeError`` against\n    a naive ``filed_at``. Comparison is therefore wall-clock, which is what the\n    local-first single-machine model wants; an offset bound is matched on its\n    wall-clock fields, not its absolute instant, so a bound whose offset differs\n    from the zone ``filed_at`` was recorded in is matched by clock time.\n    The accepted grammar is a date, an ISO timestamp (optionally fractional),\n    and an optional ``Z``/``±HH:MM`` offset; other ISO 8601 forms (basic format,\n    week dates) are outside the contract and are rejected on the Python 3.9 floor\n    even where a newer ``fromisoformat`` would accept them.\n    Blank / whitespace-only means \"no filter\" (``None``).\n    Raises ``ValueError`` on an unparseable value so the caller can surface a\n    clear error, mirroring the wing/room sanitizers.\n    \"\"\"\n    if value is None:\n        return None\n    if not isinstance(value, str):\n        raise ValueError(f\"{field_name} must be an ISO date string\")\n    value = value.strip()\n    if not value:\n        return None\n    # datetime.fromisoformat before Python 3.11 rejects a trailing \"Z\" (Zulu),\n    # and appending \"+00:00\" would break a date-only value on 3.9/3.10\n    # (\"2026-04-01+00:00\" is rejected there). Any offset is dropped below for\n    # wall-clock comparison anyway, so just strip a trailing Z/z; both date and\n    # date-time Zulu inputs then parse on the 3.9 floor.\n    iso = value[:-1] if value.endswith((\"Z\", \"z\")) else value\n    try:\n        parsed = datetime.fromisoformat(iso)\n    except ValueError as exc:\n        raise ValueError(\n            f\"{field_name} must be an ISO date string \"\n            f\"(e.g. '2026-04-01' or '2026-04-01T09:30:00'), got {value!r}\"\n        ) from exc\n    if parsed.tzinfo is not None:\n        parsed = parsed.replace(tzinfo=None)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/date_window.py#L31-L67","documentation":"ValueError from parse_date_bound (mempalace/date_window.py:49): the value passed for a date filter is not a string at all (None is allowed and means no filter; blank/whitespace strings also mean no filter). The guard exists before any parsing so non-string inputs (ints, datetimes, lists) fail fast with the field's name rather than deep inside fromisoformat.","triggerScenarios":"Passing since=20260401 (int), before=datetime.now(), or a date object instead of an ISO string to search/list APIs that accept date windows; MCP tool callers sending a JSON number instead of a string; programmatic callers reusing datetime objects from other APIs.","commonSituations":"MCP/JSON tool boundaries where types loosen (number-vs-string); refactoring call sites from datetimes to strings incompletely; Python callers assuming the API accepts datetime like other libraries do.","solutions":["Pass ISO 8601 strings: '2026-04-01' or '2026-04-01T09:30:00' (a trailing Z is tolerated).","Convert datetime objects first: dt.date().isoformat() or dt.isoformat().","Omit the parameter or pass None/'' when no filter is wanted."],"exampleFix":"# before\nresults = search(since=start_dt)  # datetime object\n\n# after\nresults = search(since=start_dt.date().isoformat())","handlingStrategy":"type-guard","validationCode":"def as_iso_or_none(value):\n    if value is None or (isinstance(value, str) and not value.strip()):\n        return None\n    if not isinstance(value, str):\n        raise TypeError(\"date filter must be an ISO string\")\n    return value","typeGuard":"from datetime import date, datetime\n\ndef is_iso_date_str(value: object) -> bool:\n    if not isinstance(value, str) or not value.strip():\n        return False\n    try:\n        datetime.fromisoformat(value.rstrip(\"Zz\"))\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    since_dt, before_dt = parse_window(since, before)\nexcept ValueError as exc:\n    raise UserInputError(str(exc)) from exc  # surface to the caller; no retry","preventionTips":["Coerce datetimes/dates to .isoformat() at the API boundary.","Validate MCP tool inputs (numbers vs strings) before calling search APIs.","Pass None or '' to express 'no filter' rather than 0 or empty objects."],"tags":["validation","dates","api-contract"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}