Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/models/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ pub fn cents_to_dollars(cents: i64) -> f64 {
cents as f64 / 100.0
}

/// Custom deserializer that treats `null` as an empty `Vec`.
///
/// The Kalshi API sometimes returns `null` instead of `[]` for empty arrays.
/// Use with `#[serde(default, deserialize_with = "null_as_empty_vec::deserialize")]`.
pub(crate) mod null_as_empty_vec {
use serde::{Deserialize, Deserializer};

pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
14 changes: 1 addition & 13 deletions src/models/market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,19 +542,7 @@ mod orderbook_serde {
}
}

/// Custom deserializer for Vec that treats null as empty.
mod null_as_empty_vec {
use serde::{Deserialize, Deserializer};

pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
let opt: Option<Vec<T>> = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
}
use super::common::null_as_empty_vec;

/// An orderbook for a market.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
Expand Down
12 changes: 1 addition & 11 deletions src/models/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -975,17 +975,7 @@ impl GetQueuePositionsParams {
}
}

mod null_as_empty_vec {
use serde::{Deserialize, Deserializer};

pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
}
}
use super::common::null_as_empty_vec;

#[cfg(test)]
mod tests {
Expand Down
7 changes: 7 additions & 0 deletions src/ws/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,13 @@ impl KalshiStreamSession {
}

Ok(IncomingMessage::Update { msg_type, sid, msg }) => {
// Normalize API aliases to canonical channel names.
// The Kalshi API sends "user_order" (singular) for order updates,
// but the channel is subscribed as "user_orders" (plural).
let msg_type = match msg_type.as_str() {
"user_order" => "user_orders".to_string(),
_ => msg_type,
};
debug!("Update on sid {}: type={}", sid, msg_type);

// Handle "unsubscribed" updates specially
Expand Down