fix(models): use plain dict for JSON columns

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.
This commit is contained in:
Padreug 2026-05-09 06:50:05 +02:00
commit f157785d22

View file

@ -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):