Skip to content

Commit 604b56c

Browse files
dmealingclaude
andcommitted
fix(codegen-python): emit Any (not str) for field.string @dbColumnType:jsonb (#98)
pg8000 auto-decodes a jsonb column to a native Python object (dict/list/ scalar) at read time, so typing a field.string @dbColumnType:jsonb field as `str` in the generated Pydantic model is a type lie (the runtime return-type integration test already asserts the value is a dict). Emit `Any` instead — the Python analogue of the TS z.unknown() fix in #97 — threading `from typing import Any` into the module imports via the existing PyType import mechanism. field.object @storage:jsonb (structured VO) and field.string @dbColumnType:uuid are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
1 parent 4e262b3 commit 604b56c

3 files changed

Lines changed: 40 additions & 0 deletions

File tree

server/python/src/metaobjects/codegen/type_map.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from metaobjects.meta.core.field.meta_field import MetaField
77
from metaobjects.meta.core.field import field_constants as fc
8+
from metaobjects.meta.persistence.db import db_constants as dbc
89
from metaobjects.shared.structural import KEY_IS_ARRAY
910

1011

@@ -94,6 +95,17 @@ def py_type_for(field: MetaField) -> PyType:
9495
base = PyType(f"Literal[{members}]", ("from typing import Literal",))
9596
else:
9697
base = PyType("str")
98+
elif (
99+
field.sub_type == fc.FIELD_SUBTYPE_STRING
100+
and field.attrs().get(dbc.FIELD_ATTR_DB_COLUMN_TYPE) == dbc.DB_COLUMN_TYPE_JSONB
101+
):
102+
# Issue #98 — a field.string carrying @dbColumnType:jsonb is the escape hatch
103+
# for a genuinely-open JSON column (ADR-0013). pg8000 auto-decodes a jsonb
104+
# column to a native Python object (dict/list/scalar) at read time, so a `str`
105+
# annotation is a type lie. Bind ``Any`` — the Python analogue of TS's
106+
# z.unknown() (#97) — since jsonb can hold any JSON value. (Other ports whose
107+
# drivers return jsonb as raw text correctly keep their string type.)
108+
base = PyType("Any", ("from typing import Any",))
97109
else:
98110
base = _SCALAR.get(field.sub_type, PyType("str"))
99111
if field_is_array(field):

server/python/tests/codegen/test_entity_model.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,17 @@ def test_field_default_is_emitted_as_literal() -> None:
175175
assert "n: int = 1" in out
176176
assert "c: float = 0.5" in out
177177
assert "s: str = 'approx'" in out
178+
179+
180+
def test_dbcolumntype_jsonb_field_is_any_with_import() -> None:
181+
"""Issue #98 — a field.string @dbColumnType:jsonb stores genuinely-open JSON,
182+
which pg8000 decodes to a native Python object at read time. The generated
183+
Pydantic field is typed ``Any`` (not the lying ``str``) and the module threads
184+
``from typing import Any`` into its import block."""
185+
from metaobjects.meta.persistence.db import db_constants as dbc
186+
187+
payload = MetaField(TYPE_FIELD, fc.FIELD_SUBTYPE_STRING, "payload")
188+
payload.set_attr(dbc.FIELD_ATTR_DB_COLUMN_TYPE, dbc.DB_COLUMN_TYPE_JSONB)
189+
out = render_entity_model(_entity("Event", [payload], package="myapp::events"))
190+
assert "from typing import Any" in out
191+
assert "payload: Any | None = None" in out

server/python/tests/codegen/test_type_map.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,17 @@ def test_dbcolumntype_uuid_string_stays_str() -> None:
102102
f = _field(fc.FIELD_SUBTYPE_STRING)
103103
f.set_attr(dbc.FIELD_ATTR_DB_COLUMN_TYPE, dbc.DB_COLUMN_TYPE_UUID)
104104
assert py_type_for(f).expr == "str"
105+
106+
107+
def test_dbcolumntype_jsonb_string_becomes_any() -> None:
108+
# Issue #98 — a field.string carrying @dbColumnType:jsonb stores genuinely-open
109+
# JSON; pg8000 auto-decodes a jsonb column to a native Python object (dict/list/
110+
# scalar), so `str` is a type lie. Emit `Any` (the Python analogue of TS's
111+
# z.unknown(), #97), threading `from typing import Any` into the module imports.
112+
from metaobjects.meta.persistence.db import db_constants as dbc
113+
114+
f = _field(fc.FIELD_SUBTYPE_STRING)
115+
f.set_attr(dbc.FIELD_ATTR_DB_COLUMN_TYPE, dbc.DB_COLUMN_TYPE_JSONB)
116+
t = py_type_for(f)
117+
assert t.expr == "Any"
118+
assert "from typing import Any" in t.imports

0 commit comments

Comments
 (0)