It might not work great with more complex usage so may not be worth it, however, the lazyload can succeed here with some more explicit casts, also the json attribute is the "foreign" part here because it's the part that references something else.
Here's your POC
from sqlalchemy import cast
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import foreign
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
Base = declarative_base()
class Department(Base):
__tablename__ = "project"
project_id = Column(Integer, autoincrement=True, primary_key=True)
meta = Column(MutableDict.as_mutable(JSONB), nullable=False, default={})
department = relationship(
"Department",
viewonly=True,
primaryjoin=lambda: Department.department_id
== foreign(
cast(Project.meta, JSONB)["department_id"].astext.cast(Integer)
),
)
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.drop_all(e)
Base.metadata.create_all(e)
s = Session(e)
d1 = Department(department_id=1)
s.add(d1)
s.add(Project(meta={"department_id": 1}))
s.commit()
p1 = s.query(Project).first()
assert p1.department is d1