Skip to content

Msgspec Doesn't Decode Json Correctly when SQLAlchemy.Mapped is used. #820

Open
@Vizonex

Description

@Vizonex

Description

I am having a problem where when I want to write a Json Decoder for SQLAlchemy but then I bump into an issue where msgspec doesn't know how to handle these types.

from sqlalchemy.orm import Mapped, MappedAsDataclass, DeclarativeBaseNoMeta, mapped_column, declared_attr
from msgspec.json import Decoder, Encoder


class SQLTable(MappedAsDataclass, DeclarativeBaseNoMeta):
    pass


class User(SQLTable):
    __tablename__ = "user"
    # Mapped is used to register columns in the sql table without 
    # Having to write mapped_column out many times.
    name:Mapped[str]
    id:Mapped[int] = mapped_column(primary_key=True, default=None)

encoder = Encoder()
# Encoder works just fine...
print(encoder.encode(User("Vizonex", 10)))


# But the decoder on the other-hand is broken and I expect to decode the User json 
# To a User Object...
# msgspec.ValidationError: Expected `Mapped`, got `str` - at `$.name`
user = Decoder(User).decode(b'{"name":"Vizonex", "id": 10}')

I am completely stumped on how to fix this problem and I have gone through all hoops and hurtles to try and make the decoder for my upcoming python library SQLTable to decode json correctly.

Other Things I've Tried

I have tried hacking the Mapped[] Object itself into an annotated object but then SQLAlchemy fails to find it...

from sqlalchemy.orm import Mapped, MappedAsDataclass, DeclarativeBaseNoMeta, mapped_column, declared_attr
from typing import Annotated, TypeVar
from msgspec.json import Decoder, Encoder

T = TypeVar("T")

# this was an idea I came up with to allow msgspec to act friendly with 
# Sqlalchemy Mapped variables but sqlalchmey doesn't act friendly with 
# finding Mapped attributes...
HackedMap = Annotated[T, Mapped[T]]

class SQLTable(MappedAsDataclass, DeclarativeBaseNoMeta):
    pass

class User(SQLTable):
    __tablename__ = "user"
    # We can try to hack it to how we please so that msgspec's decoder works correctly 
    # but then a new problem arises     
    name:HackedMap[str]
    id:HackedMap[int] = mapped_column(primary_key=True, default=None)



# It only shows {'id': <ColumnProperty at 0x20d09b14e40; id>} Because we set it with mapped_column
print(User.__mapper__.attrs._data)

I am almost completely stumped on how this will get solved. But there are 2 fixes that I could think of that could be approched to solve this once and for all.

  1. SQLAlchemy adds some new logic for finding mapped variables hidden inside Annotated types.
  2. The way we could fix _core.c involves fixing the function typenode_collect_type function where we could check if the class has a __table__ or another obscure type belonging to sqlalchemy and then try to filter out Mapped[T] types through that kind of Boolean/logic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions