From f157785d223ad2ca7e09db89241fb49357f3f561 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 9 May 2026 06:50:05 +0200 Subject: [PATCH] fix(models): use plain `dict` for JSON columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LNbits's db.dict_to_model walks each field and unconditionally calls `issubclass(field.type_, bool)` (lnbits/db.py:730). When field.type_ is a parameterized generic alias like `list[dict[str, str]]` (the value type of OpenHours.schedule's `dict[str, list[dict[str, str]]]`), Python raises: TypeError: issubclass() arg 1 must be a class — which surfaced as a 500 on GET /restaurants and any other endpoint that deserialized a Restaurant row. POSTs succeeded (serialization writes JSON), but every read crashed. So newly created restaurants disappeared from the list and the settings page 500'd. Same risk on RestaurantExtra.fields, MenuItemExtra.fields, and OrderExtra.fields (all `dict[str, str]`) — those didn't crash but silently failed to round-trip (the JSON string stayed a string instead of being parsed back to a dict). Loosen all four fields to plain `dict`. With type_=dict, LNbits's deserializer hits its 'type_ is dict' branch (lnbits/db.py:744) and json.loads the value correctly. The runtime shape is unchanged; we just lose static type parameterization on these JSON columns. Documented inline so future contributors don't tighten them back. --- models.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/models.py b/models.py index e09c445..a71d5bd 100644 --- a/models.py +++ b/models.py @@ -53,10 +53,18 @@ class OpenHours(BaseModel): """Weekly opening schedule. Weekday key 0=Mon .. 6=Sun. Each day is a list of {start, end} ranges so a venue can be open - e.g. 11:00-15:00 and 18:00-23:00 in the same day. + e.g. 11:00-15:00 and 18:00-23:00 in the same day. Persisted as + JSON in the DB. + + Typed as plain `dict` (not `dict[str, list[dict[str, str]]]`) so + LNbits's `db.dict_to_model` walks the field cleanly: its + introspection calls `issubclass(field.type_, bool)` while + iterating, and a parameterized generic alias trips it with + "issubclass() arg 1 must be a class". The runtime shape is + still the dict-of-lists-of-dicts described above. """ - schedule: dict[str, list[dict[str, str]]] = Field(default_factory=dict) + schedule: dict = Field(default_factory=dict) class SocialLinks(BaseModel): @@ -69,7 +77,10 @@ class SocialLinks(BaseModel): class RestaurantExtra(BaseModel): notes: Optional[str] = None - fields: dict[str, str] = Field(default_factory=dict) + # Typed as plain `dict` (not `dict[str, str]`) so LNbits's + # `db.dict_to_model` round-trips it cleanly — see OpenHours for + # the same workaround and rationale. + fields: dict = Field(default_factory=dict) class CreateRestaurant(BaseModel): @@ -211,7 +222,8 @@ class MenuItemExtra(BaseModel): """Free-form metadata that doesn't deserve a column yet.""" notes: Optional[str] = None - fields: dict[str, str] = Field(default_factory=dict) + # Plain `dict` — see OpenHours for the LNbits round-trip workaround. + fields: dict = Field(default_factory=dict) class CreateMenuItem(BaseModel): @@ -404,7 +416,8 @@ class OrderExtra(BaseModel): fiat_currency: Optional[str] = None fiat_rate: Optional[float] = None refund_address: Optional[str] = None - fields: dict[str, str] = Field(default_factory=dict) + # Plain `dict` — see OpenHours for the LNbits round-trip workaround. + fields: dict = Field(default_factory=dict) class CreateOrder(BaseModel):