Adapting types to fix SQLAlchemy's "can't adapt type" error
I have recently been using Pydantic a lot, which is pretty cool since it lets me specify concrete types on my previously untyped data.
Until I ran into an error when I was inserting data stored in a Pydantic model to my postgres database using SQLAlchemy.
Problem #
I have a Pydantic model like this to define my incoming data from a request:
# Pydantic class
class EventSchema(BaseModel):
ip_address: IPvAnyAddress
Another part in my code defined an SQLAlchemy model to persist this incoming information (along with other data not shown here):
# SQLAlchemy model
class Event(Base):
__tablename__ = "event"
id = Column(Integer, primary_key=True)
ip_address = Column(INET)
To convert the data from EventSchema
to Event
, I wrote some glue code:
# Glue code
event_data = EventSchema(ip_address="127.0.0.1")
event = Event(**event_data.dict())
Unfortunately, this failed with an error when I tried to commit the event
object to the database.
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'IPv4Address'
[SQL: INSERT INTO event (ip_address) VALUES (%(ip_address)s) RETURNING event.id]
[parameters: {'ip_address': IPv4Address('127.0.0.1')}]
(Background on this error at: http://sqlalche.me/e/13/f405)
If you visit that link, it’s not very helpful. Just a general text about ProgrammingError
error. Which makes sense if you think about it: it’s an error outside of SQLAlchemy’s control.
The description is clear however. The IPv4Address
Pydantic type can’t be used as a value in the database.
Naive solution: Converting the type #
While I could always convert the data manually by converting the IPv4Address type manually to a string, I wanted to avoid that. It’s an easy solution, but if I came across more Pydantic types that could not be adapted, I’d have to remember to convert each of them manually.
Better solution: Adapting unknown types #
As usual with the SQLAlchemy ecosystem there’s an elegant way to achieve this by using the register_adapter
handler in psycopg2, which in this case is the database adapter used by SQLAlchemy to interface with postgres.
from psycopg2.extensions import register_adapter, AsIs
from pydantic.networks import IPv4Address, IPv6Address
def adapt_pydantic_ip_address(ip):
return AsIs(repr(ip.exploded))
register_adapter(IPv4Address, adapt_pydantic_ip_address)
register_adapter(IPv6Address, adapt_pydantic_ip_address)
I put this code next to where I set up my SQLAlchemy session. But it does not matter where you choose place the adapters.
This turned out to be a very elegant solution since I only have to do it once and forget about having to do it again. Plus I can always adapt more Pydantic types if needed using a similar approach.