Documentation Index
Fetch the complete documentation index at: https://mintlify.com/launchbadge/sqlx/llms.txt
Use this file to discover all available pages before exploring further.
SQLx’s type system is open: any Rust type can participate in parameter binding and row decoding as long as it implements the three core traits. This page walks through implementing those traits by hand, covering simple newtypes, enumerations, and PostgreSQL-specific constructs like custom enum types and composite types.
For most use cases you do not need to implement these traits manually. The #[derive(Type)], #[derive(Encode)], and #[derive(Decode)] macros generate correct implementations with far less boilerplate. See Derive macros for details.
Implementing the Type trait
Every custom type must tell SQLx which SQL type it corresponds to. Implement Type<DB> to provide that mapping:
use sqlx::Database;
struct Meters(f64);
impl<DB: Database> sqlx::Type<DB> for Meters
where
f64: sqlx::Type<DB>,
{
fn type_info() -> DB::TypeInfo {
<f64 as sqlx::Type<DB>>::type_info()
}
}
Here Meters delegates to f64’s type info, so SQLx will send and receive it as a DOUBLE PRECISION column. For database-specific types such as PostgreSQL UUID or a custom enum, you would return the concrete PgTypeInfo for that type instead.
Implementing Encode
Encode writes the Rust value into the argument buffer that SQLx transmits to the database. The simplest approach is to convert your type to a supported primitive and delegate:
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
impl<'q, DB: Database> sqlx::Encode<'q, DB> for Meters
where
f64: sqlx::Encode<'q, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as Database>::ArgumentBuffer,
) -> Result<IsNull, BoxDynError> {
<f64 as sqlx::Encode<DB>>::encode_by_ref(&self.0, buf)
}
}
If your type can be moved without extra cost, also override encode to avoid unnecessary copies:
fn encode(
self,
buf: &mut <DB as Database>::ArgumentBuffer,
) -> Result<IsNull, BoxDynError> {
<f64 as sqlx::Encode<DB>>::encode(self.0, buf)
}
The IsNull enum
Return IsNull::Yes when your value should be stored as SQL NULL. Return IsNull::No otherwise. For Option<T>, the standard library implementation already handles None → IsNull::Yes.
use sqlx::encode::IsNull;
// nullable wrapper example
fn encode_option(value: Option<f64>, buf: &mut impl AsMut<Vec<u8>>) -> IsNull {
match value {
Some(v) => {
// ... write v into buf
IsNull::No
}
None => IsNull::Yes,
}
}
Implementing Decode
Decode reads a column value from the database’s wire format into your Rust type. The common pattern is to decode from a supported intermediate type:
impl<'r, DB: Database> sqlx::Decode<'r, DB> for Meters
where
f64: sqlx::Decode<'r, DB>,
{
fn decode(
value: <DB as Database>::ValueRef<'r>,
) -> Result<Self, BoxDynError> {
let inner = <f64 as sqlx::Decode<DB>>::decode(value)?;
Ok(Meters(inner))
}
}
You can also decode through &str when your type is stored as text:
use std::str::FromStr;
struct Color(u8, u8, u8);
impl<'r, DB: Database> sqlx::Decode<'r, DB> for Color
where
&'r str: sqlx::Decode<'r, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
let s = <&str as sqlx::Decode<DB>>::decode(value)?;
// parse "#rrggbb"
let hex = s.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
Ok(Color(r, g, b))
}
}
Newtype wrappers
A newtype around an existing SQLx type is the simplest custom type. Implement all three traits by forwarding to the inner type:
#[derive(Debug)]
struct UserId(i64);
impl<DB: Database> sqlx::Type<DB> for UserId
where
i64: sqlx::Type<DB>,
{
fn type_info() -> DB::TypeInfo {
<i64 as sqlx::Type<DB>>::type_info()
}
}
impl<'q, DB: Database> sqlx::Encode<'q, DB> for UserId
where
i64: sqlx::Encode<'q, DB>,
{
fn encode_by_ref(
&self,
buf: &mut DB::ArgumentBuffer,
) -> Result<IsNull, BoxDynError> {
self.0.encode_by_ref(buf)
}
}
impl<'r, DB: Database> sqlx::Decode<'r, DB> for UserId
where
i64: sqlx::Decode<'r, DB>,
{
fn decode(value: DB::ValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(UserId(<i64 as sqlx::Decode<DB>>::decode(value)?))
}
}
In practice, #[derive(sqlx::Type)] with #[sqlx(transparent)] generates this exact pattern for you.
PostgreSQL enum types
PostgreSQL allows you to define custom enum types in the database. SQLx can encode and decode Rust enums against them.
Creating the SQL type
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
Implementing Type for a PostgreSQL enum
For PostgreSQL, use PgTypeInfo to declare the SQL type name:
use sqlx::postgres::{PgHasArrayType, PgTypeInfo};
#[derive(Debug)]
enum Mood {
Happy,
Sad,
Neutral,
}
impl sqlx::Type<sqlx::Postgres> for Mood {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("mood")
}
}
Encoding and decoding via string
Encode the variant as its lowercase string name and decode by parsing back:
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::PgArgumentBuffer;
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Mood {
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
let s = match self {
Mood::Happy => "happy",
Mood::Sad => "sad",
Mood::Neutral => "neutral",
};
<&str as sqlx::Encode<sqlx::Postgres>>::encode_by_ref(&s, buf)
}
}
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Mood {
fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, BoxDynError> {
let s = <&str as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
match s {
"happy" => Ok(Mood::Happy),
"sad" => Ok(Mood::Sad),
"neutral" => Ok(Mood::Neutral),
other => Err(format!("unknown mood: {other}").into()),
}
}
}
The PgHasArrayType trait
To use your custom type inside a PostgreSQL array (e.g. mood[]), implement PgHasArrayType:
impl PgHasArrayType for Mood {
fn array_type_info() -> PgTypeInfo {
PgTypeInfo::with_name("_mood") // PostgreSQL array types are prefixed with _
}
}
With this in place, Vec<Mood> becomes encodable and decodable as a mood[] column.
The #[derive(sqlx::Type)] macro generates PgHasArrayType automatically for transparent newtypes and PostgreSQL enum types, so you rarely need to write it by hand.
PostgreSQL composite types
PostgreSQL composite (record) types map to Rust structs. Implementing Type, Encode, and Decode for a composite type involves reading and writing each field in order.
CREATE TYPE inventory_item AS (
name TEXT,
quantity INT
);
use sqlx::postgres::{PgTypeInfo, PgArgumentBuffer, PgValueRef, PgHasArrayType};
struct InventoryItem {
name: String,
quantity: i32,
}
impl sqlx::Type<sqlx::Postgres> for InventoryItem {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("inventory_item")
}
}
Full composite type encoding and decoding requires interacting with PostgreSQL’s binary format. For most cases, prefer #[derive(sqlx::Type)] on a struct with #[sqlx(type_name = "inventory_item")], which handles the binary protocol automatically (PostgreSQL only).
Using custom types in queries
Once all three traits are implemented, the type works seamlessly with query and query_as:
// Binding a custom type as a parameter
let user = sqlx::query_as::<_, User>(
"SELECT id, name FROM users WHERE mood = $1"
)
.bind(Mood::Happy)
.fetch_one(&pool)
.await?;
// Reading a custom type from a column
let mood: Mood = sqlx::query_scalar("SELECT mood FROM users WHERE id = $1")
.bind(42_i32)
.fetch_one(&pool)
.await?;
When using query! or query_as! with custom types, you must use a type override in the SQL string because the macro has no knowledge of user-defined types:
let user = sqlx::query_as!(
User,
r#"SELECT id, name, mood AS "mood: Mood" FROM users WHERE id = $1"#,
42_i32
)
.fetch_one(&pool)
.await?;