pydantic-sqlalchemy icon indicating copy to clipboard operation
pydantic-sqlalchemy copied to clipboard

Support for complex custom column types

Open rollcat opened this issue 4 years ago • 1 comments

Currently the conversion inspects the impl attribute of the column's type to derive the field type for Pydantic, which may be a different Python type from what the SQLAlchemy model actually uses (via process_bind_param, process_result_value, etc).

For example, I have this custom, BLOB-backed UUID type, which I use with SQLite:

import uuid
from sqlalchemy.types import BLOB, TypeDecorator

class UUID(TypeDecorator):
    impl = BLOB

    def load_dialect_impl(self, dialect):
        return dialect.type_descriptor(BLOB(16))

    def process_bind_param(self, value, dialect):
        if value is None:
            return value
        if not isinstance(value, uuid.UUID):
            value = uuid.UUID(value)
        return value.bytes

    def process_result_value(self, value, dialect):
        if value is None:
            return value
        if isinstance(value, bytes):
            return uuid.UUID(bytes=value)
        if isinstance(value, uuid.UUID):
            return value
        raise TypeError(type(value))

This correctly gets me an uuid.UUID instance in and out of the DB, but the corresponding Pydantic model uses a bytes type.

It seems SQLAlchemy doesn't have a mechanism to directly specify the mapped Python type, otherwise we would be able to write something like this:

...
class UUID(TypeDecorator):
    impl = BLOB
    python_type = uuid.UUID
    ...

And then adapt the logic in sqlalchemy_to_pydantic:

                if hasattr(column.type, "python_type"):
                    python_type = column.type.python_type
                elif hasattr(column.type, "impl"):
                    if hasattr(column.type.impl, "python_type"):
                        python_type = column.type.impl.python_type
                elif hasattr(column.type, "python_type"):
                    python_type = column.type.python_type

It would also allow monkey-patching any existing or third-party column types (also see #6).

(Or perhaps the more correct approach would be to create a custom impl? I'm not sure, seems that's why we have process_bind_param, process_result_value, copy, etc)

rollcat avatar Oct 29 '20 13:10 rollcat

I have the same problem with uuid thats implement LargeBinary, I'm getting the following error:

response -> 0 -> id
  byte type expected (type=type_error.bytes)

And I have my custom implementation :

class UUID(TypeDecorator):
    """
    UUID Binary type decorator
    """
    impl = LargeBinary

e-belair avatar Jan 12 '21 14:01 e-belair