Migrate PostgresDsn.build from pydentic v1 to pydantic v2
Question:
I have simple Config class from FastAPI tutorial. But it seems like it uses old pydantic version. I run my code with pydantic v2 version and get a several errors. I fix almost all of them, but the last one I cannot fix yet. This is part of code which does not work:
from pydantic import AnyHttpUrl, HttpUrl, PostgresDsn, field_validator
from pydantic_settings import BaseSettings
from pydantic_core.core_schema import FieldValidationInfo
load_dotenv()
class Settings(BaseSettings):
...
POSTGRES_SERVER: str = 'localhost:5432'
POSTGRES_USER: str = os.getenv('POSTGRES_USER')
POSTGRES_PASSWORD: str = os.getenv('POSTGRES_PASSWORD')
POSTGRES_DB: str = os.getenv('POSTGRES_DB')
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode='before')
@classmethod
def assemble_db_connection(cls, v: Optional[str], info: FieldValidationInfo) -> Any:
if isinstance(v, str):
return v
postgres_dsn = PostgresDsn.build(
scheme="postgresql",
username=info.data.get("POSTGRES_USER"),
password=info.data.get("POSTGRES_PASSWORD"),
host=info.data.get("POSTGRES_SERVER"),
path=f"{info.data.get('POSTGRES_DB') or ''}",
)
return str(postgres_dsn)
That is the error which I get:
sqlalchemy.exc.ArgumentError: Expected string or URL object, got MultiHostUrl('postgresql://user:password@localhost:5432/database')
I check a lot of places, but cannot find how I can fix that, it looks like build
method pass data to the sqlalchemy create_engine
method as a MultiHostUrl
instance instead of string. How should I properly migrate this code to use pydantic v2?
UPDATE
I have fixed that issue by changing typing for SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
to SQLALCHEMY_DATABASE_URI: Optional[str] = None
.
Because pydantic makes auto conversion of result for some reason. But I am not sure if that approach is the right one, maybe there are better way to do that?
Answers:
Pydantic V2 BaseSettings will automatically load values from the environment, based on prefix
:
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='POSTGRES_', case_sensitive=False)
server: str = Field(default='localhost:5432')
user: str = 'xxx'
password: str = 'xxx'
db: str = 'xxx'
user: str = 'xxx'
sqlalchemy_database_uri: Optional[PostgresDsn]
@field_validator("SQLALCHEMY_DATABASE_URI", mode='after')
def assemble_db_connection(cls, v: Optional[Union[PostgresDsn, str]], info: FieldValidationInfo) -> PostgresDsn:
if isinstance(v, (str, PostgresDsn)):
return v
else:
return PostgresDsn(
scheme="postgresql",
username=cls.user,
password=cls.password,
host=cls.server,
path=cls.db,
)
Also, your validator should operate after
validation, since values need to be loaded first.
I have met the same issue and fixed it with tipecasting on engine create:
from sqlalchemy.ext.asyncio import create_async_engine
create_async_engine(str(settings.POSTGRES_URI))
You can use unicode_string()
to stringify your URI
as follow:
from sqlalchemy.ext.asyncio import create_async_engine
create_async_engine(settings.POSTGRES_URI.unicode_string())
Check documentation page here for additional explanation.
I had the same problem and eventually came up with the following migration:
Pydantic V1:
from pydantic import BaseSettings, PostgresDsn, validator
class Settings(BaseSettings):
POSTGRES_SERVER: Optional[str]
POSTGRES_USER: Optional[str]
POSTGRES_PASSWORD: Optional[str]
POSTGRES_DB: Optional[str]
SQLALCHEMY_DATABASE_URI: Union[Optional[PostgresDsn], Optional[str]] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
print("Loading SQLALCHEMY_DATABASE_URI from docker.env file ...")
return v
print("Creating SQLALCHEMY_DATABASE_URI from .env file ...")
return PostgresDsn.build(
scheme="postgresql",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
class Config:
env_file = ".env"
case_sensitive = True
Pydantic V2:
from pydantic import PostgresDsn, field_validator, ValidationInfo
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
POSTGRES_SERVER: Optional[str] = None
POSTGRES_USER: Optional[str] = None
POSTGRES_PASSWORD: Optional[str] = None
POSTGRES_DB: Optional[str] = None
SQLALCHEMY_DATABASE_URI: Union[Optional[PostgresDsn], Optional[str]] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
@classmethod
def assemble_db_connection(cls, v: Optional[str], values: ValidationInfo) -> Any:
if isinstance(v, str):
print("Loading SQLALCHEMY_DATABASE_URI from .docker.env file ...")
return v
print("Creating SQLALCHEMY_DATABASE_URI from .env file ...")
return PostgresDsn.build(
scheme="postgresql",
username=values.data.get("POSTGRES_USER"),
password=values.data.get("POSTGRES_PASSWORD"),
host=values.data.get("POSTGRES_SERVER"),
path=f"{values.data.get('POSTGRES_DB') or ''}",
)
Also, for calling the settings
instance objects you need casting to string in V2:
settings.SQLALCHEMY_DATABASE_URI.unicode_string()
# or
f"{settings.SQLALCHEMY_DATABASE_URI}
I ran into the same problem and this is how I fixed it.
No other changes were needed throughout the codebase.
Pydantic V2:
from typing import Optional
from pydantic import PostgresDsn, field_validator, ValidationInfo
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_HOST: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[str] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
@classmethod
def assemble_db_uri(cls, field_value, info: ValidationInfo) -> str:
if isinstance(field_value, str):
return field_value
return PostgresDsn.build(
scheme="postgresql+psycopg2",
username=info.data.get("POSTGRES_USER"),
password=info.data.get("POSTGRES_PASSWORD"),
host=info.data.get("POSTGRES_HOST"),
path=info.data.get("POSTGRES_DB") or "",
).unicode_string()
Pydantic V1:
from typing import Any, Dict, Optional
from pydantic import BaseSettings, PostgresDsn, validator
class Settings(BaseSettings):
POSTGRES_HOST: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+psycopg2",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_HOST"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
Here’s the diff:
> diff -u pydantic_v1.py pydantic_v2.py
--- pydantic_v1.py
+++ pydantic_v2.py
@@ -1,5 +1,6 @@
-from typing import Any, Dict, Optional
-from pydantic import BaseSettings, PostgresDsn, validator
+from typing import Optional
+from pydantic import PostgresDsn, field_validator, ValidationInfo
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
@@ -7,16 +8,17 @@
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
- SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
+ SQLALCHEMY_DATABASE_URI: Optional[str] = None
- @validator("SQLALCHEMY_DATABASE_URI", pre=True)
- def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
- if isinstance(v, str):
- return v
+ @field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
+ @classmethod
+ def assemble_db_uri(cls, field_value, info: ValidationInfo) -> str:
+ if isinstance(field_value, str):
+ return field_value
return PostgresDsn.build(
scheme="postgresql+psycopg2",
- user=values.get("POSTGRES_USER"),
- password=values.get("POSTGRES_PASSWORD"),
- host=values.get("POSTGRES_HOST"),
- path=f"/{values.get('POSTGRES_DB') or ''}",
- )
+ username=info.data.get("POSTGRES_USER"),
+ password=info.data.get("POSTGRES_PASSWORD"),
+ host=info.data.get("POSTGRES_HOST"),
+ path=info.data.get("POSTGRES_DB") or "",
+ ).unicode_string()
I have simple Config class from FastAPI tutorial. But it seems like it uses old pydantic version. I run my code with pydantic v2 version and get a several errors. I fix almost all of them, but the last one I cannot fix yet. This is part of code which does not work:
from pydantic import AnyHttpUrl, HttpUrl, PostgresDsn, field_validator
from pydantic_settings import BaseSettings
from pydantic_core.core_schema import FieldValidationInfo
load_dotenv()
class Settings(BaseSettings):
...
POSTGRES_SERVER: str = 'localhost:5432'
POSTGRES_USER: str = os.getenv('POSTGRES_USER')
POSTGRES_PASSWORD: str = os.getenv('POSTGRES_PASSWORD')
POSTGRES_DB: str = os.getenv('POSTGRES_DB')
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode='before')
@classmethod
def assemble_db_connection(cls, v: Optional[str], info: FieldValidationInfo) -> Any:
if isinstance(v, str):
return v
postgres_dsn = PostgresDsn.build(
scheme="postgresql",
username=info.data.get("POSTGRES_USER"),
password=info.data.get("POSTGRES_PASSWORD"),
host=info.data.get("POSTGRES_SERVER"),
path=f"{info.data.get('POSTGRES_DB') or ''}",
)
return str(postgres_dsn)
That is the error which I get:
sqlalchemy.exc.ArgumentError: Expected string or URL object, got MultiHostUrl('postgresql://user:password@localhost:5432/database')
I check a lot of places, but cannot find how I can fix that, it looks like build
method pass data to the sqlalchemy create_engine
method as a MultiHostUrl
instance instead of string. How should I properly migrate this code to use pydantic v2?
UPDATE
I have fixed that issue by changing typing for SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
to SQLALCHEMY_DATABASE_URI: Optional[str] = None
.
Because pydantic makes auto conversion of result for some reason. But I am not sure if that approach is the right one, maybe there are better way to do that?
Pydantic V2 BaseSettings will automatically load values from the environment, based on prefix
:
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='POSTGRES_', case_sensitive=False)
server: str = Field(default='localhost:5432')
user: str = 'xxx'
password: str = 'xxx'
db: str = 'xxx'
user: str = 'xxx'
sqlalchemy_database_uri: Optional[PostgresDsn]
@field_validator("SQLALCHEMY_DATABASE_URI", mode='after')
def assemble_db_connection(cls, v: Optional[Union[PostgresDsn, str]], info: FieldValidationInfo) -> PostgresDsn:
if isinstance(v, (str, PostgresDsn)):
return v
else:
return PostgresDsn(
scheme="postgresql",
username=cls.user,
password=cls.password,
host=cls.server,
path=cls.db,
)
Also, your validator should operate after
validation, since values need to be loaded first.
I have met the same issue and fixed it with tipecasting on engine create:
from sqlalchemy.ext.asyncio import create_async_engine
create_async_engine(str(settings.POSTGRES_URI))
You can use unicode_string()
to stringify your URI
as follow:
from sqlalchemy.ext.asyncio import create_async_engine
create_async_engine(settings.POSTGRES_URI.unicode_string())
Check documentation page here for additional explanation.
I had the same problem and eventually came up with the following migration:
Pydantic V1:
from pydantic import BaseSettings, PostgresDsn, validator
class Settings(BaseSettings):
POSTGRES_SERVER: Optional[str]
POSTGRES_USER: Optional[str]
POSTGRES_PASSWORD: Optional[str]
POSTGRES_DB: Optional[str]
SQLALCHEMY_DATABASE_URI: Union[Optional[PostgresDsn], Optional[str]] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
print("Loading SQLALCHEMY_DATABASE_URI from docker.env file ...")
return v
print("Creating SQLALCHEMY_DATABASE_URI from .env file ...")
return PostgresDsn.build(
scheme="postgresql",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
class Config:
env_file = ".env"
case_sensitive = True
Pydantic V2:
from pydantic import PostgresDsn, field_validator, ValidationInfo
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
POSTGRES_SERVER: Optional[str] = None
POSTGRES_USER: Optional[str] = None
POSTGRES_PASSWORD: Optional[str] = None
POSTGRES_DB: Optional[str] = None
SQLALCHEMY_DATABASE_URI: Union[Optional[PostgresDsn], Optional[str]] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
@classmethod
def assemble_db_connection(cls, v: Optional[str], values: ValidationInfo) -> Any:
if isinstance(v, str):
print("Loading SQLALCHEMY_DATABASE_URI from .docker.env file ...")
return v
print("Creating SQLALCHEMY_DATABASE_URI from .env file ...")
return PostgresDsn.build(
scheme="postgresql",
username=values.data.get("POSTGRES_USER"),
password=values.data.get("POSTGRES_PASSWORD"),
host=values.data.get("POSTGRES_SERVER"),
path=f"{values.data.get('POSTGRES_DB') or ''}",
)
Also, for calling the settings
instance objects you need casting to string in V2:
settings.SQLALCHEMY_DATABASE_URI.unicode_string()
# or
f"{settings.SQLALCHEMY_DATABASE_URI}
I ran into the same problem and this is how I fixed it.
No other changes were needed throughout the codebase.
Pydantic V2:
from typing import Optional
from pydantic import PostgresDsn, field_validator, ValidationInfo
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_HOST: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[str] = None
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
@classmethod
def assemble_db_uri(cls, field_value, info: ValidationInfo) -> str:
if isinstance(field_value, str):
return field_value
return PostgresDsn.build(
scheme="postgresql+psycopg2",
username=info.data.get("POSTGRES_USER"),
password=info.data.get("POSTGRES_PASSWORD"),
host=info.data.get("POSTGRES_HOST"),
path=info.data.get("POSTGRES_DB") or "",
).unicode_string()
Pydantic V1:
from typing import Any, Dict, Optional
from pydantic import BaseSettings, PostgresDsn, validator
class Settings(BaseSettings):
POSTGRES_HOST: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+psycopg2",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_HOST"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
Here’s the diff:
> diff -u pydantic_v1.py pydantic_v2.py
--- pydantic_v1.py
+++ pydantic_v2.py
@@ -1,5 +1,6 @@
-from typing import Any, Dict, Optional
-from pydantic import BaseSettings, PostgresDsn, validator
+from typing import Optional
+from pydantic import PostgresDsn, field_validator, ValidationInfo
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
@@ -7,16 +8,17 @@
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
- SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
+ SQLALCHEMY_DATABASE_URI: Optional[str] = None
- @validator("SQLALCHEMY_DATABASE_URI", pre=True)
- def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
- if isinstance(v, str):
- return v
+ @field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
+ @classmethod
+ def assemble_db_uri(cls, field_value, info: ValidationInfo) -> str:
+ if isinstance(field_value, str):
+ return field_value
return PostgresDsn.build(
scheme="postgresql+psycopg2",
- user=values.get("POSTGRES_USER"),
- password=values.get("POSTGRES_PASSWORD"),
- host=values.get("POSTGRES_HOST"),
- path=f"/{values.get('POSTGRES_DB') or ''}",
- )
+ username=info.data.get("POSTGRES_USER"),
+ password=info.data.get("POSTGRES_PASSWORD"),
+ host=info.data.get("POSTGRES_HOST"),
+ path=info.data.get("POSTGRES_DB") or "",
+ ).unicode_string()