Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.

Commit d2ed1b1

Browse files
moeuuclaude
andcommitted
serde: accept JSON numbers as Decimal in CLOB/data/gamma responses
Introduce a shared DecimalFromAny serde_with adapter in src/serde_helpers.rs that accepts strings, integers, and floats when deserializing rust_decimal::Decimal, and apply it to the response fields where the Polymarket API returns JSON numbers rather than strings. Impact today: ClobClient::tick_size (called transitively by limit_order().build() / market_order().build()) fails against the live /tick-size endpoint because {"minimum_tick_size": 0.01} comes back as a JSON number, so any live order placement errors out before signing. data::Client::positions and gamma::Client::markets hit the same class of bug on avgPrice, lastTradePrice, bestBid/bestAsk, volume fields, liquidity fields, and price-change fields. Covered structs: - TickSize (clob): deserializer now goes through DecimalFromAny. - Position, ClosedPosition, Trade (data): all Decimal fields annotated. ClosedPosition gains its missing #[serde_as] struct marker. - Market and related (gamma): 56 Option<Decimal> fields annotated. Backwards compatible: DecimalFromAny still accepts string-encoded Decimals, so responses that already work continue to work. No new dependencies. No feature flag changes. Verified: cargo build / cargo test pass under --features "clob,data,gamma,heartbeats,tracing" with no additional failures introduced. Live reproduction of the pre-patch error: let client = ClobClient::new("https://clob.polymarket.com", ClobConfig::default())?; client.tick_size(some_real_token_id).await?; // Err: invalid type: floating point `0.01`, expected a Decimal type representing a fixed-point number and after the patch the same call returns TickSize::Hundredth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 77264a4 commit d2ed1b1

4 files changed

Lines changed: 187 additions & 2 deletions

File tree

src/clob/types/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,14 @@ impl<'de> Deserialize<'de> for TickSize {
417417
where
418418
D: Deserializer<'de>,
419419
{
420-
let dec = <Decimal as Deserialize>::deserialize(deserializer)?;
420+
// The Polymarket API returns `minimum_tick_size` as a JSON number
421+
// (e.g. `0.01`), which `Decimal`'s default deserializer rejects
422+
// because it expects a string. Delegate to the shared flexible
423+
// Decimal deserializer so we handle both representations.
424+
use serde_with::DeserializeAs;
425+
let dec = <crate::serde_helpers::DecimalFromAny as DeserializeAs<Decimal>>::deserialize_as(
426+
deserializer,
427+
)?;
421428
TickSize::try_from(dec).map_err(de::Error::custom)
422429
}
423430
}

src/data/types/response.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use chrono::{DateTime, NaiveDate, Utc};
77
use serde::{Deserialize, Deserializer};
88
use serde_with::{DefaultOnNull, DisplayFromStr, NoneAsEmptyString, serde_as};
99

10+
use crate::serde_helpers::DecimalFromAny;
11+
1012
use super::{ActivityType, Side};
1113
use crate::types::{Address, B256, Decimal, U256};
1214

@@ -74,24 +76,34 @@ pub struct Position {
7476
/// The market condition ID (unique market identifier).
7577
pub condition_id: B256,
7678
/// Number of outcome tokens held.
79+
#[serde_as(as = "DecimalFromAny")]
7780
pub size: Decimal,
7881
/// Average entry price for the position.
82+
#[serde_as(as = "DecimalFromAny")]
7983
pub avg_price: Decimal,
8084
/// Initial value (cost basis) of the position.
85+
#[serde_as(as = "DecimalFromAny")]
8186
pub initial_value: Decimal,
8287
/// Current market value of the position.
88+
#[serde_as(as = "DecimalFromAny")]
8389
pub current_value: Decimal,
8490
/// Unrealized cash profit/loss.
91+
#[serde_as(as = "DecimalFromAny")]
8592
pub cash_pnl: Decimal,
8693
/// Unrealized percentage profit/loss.
94+
#[serde_as(as = "DecimalFromAny")]
8795
pub percent_pnl: Decimal,
8896
/// Total amount bought (cumulative).
97+
#[serde_as(as = "DecimalFromAny")]
8998
pub total_bought: Decimal,
9099
/// Realized profit/loss from closed portions.
100+
#[serde_as(as = "DecimalFromAny")]
91101
pub realized_pnl: Decimal,
92102
/// Realized percentage profit/loss.
103+
#[serde_as(as = "DecimalFromAny")]
93104
pub percent_realized_pnl: Decimal,
94105
/// Current market price of the outcome.
106+
#[serde_as(as = "DecimalFromAny")]
95107
pub cur_price: Decimal,
96108
/// Whether the position can be redeemed (market resolved).
97109
pub redeemable: bool,
@@ -129,6 +141,7 @@ pub struct Position {
129141
///
130142
/// Returned by the `/closed-positions` endpoint. Represents positions that
131143
/// have been fully sold or redeemed, with final profit/loss figures.
144+
#[serde_as]
132145
#[derive(Debug, Clone, Deserialize, Builder)]
133146
#[serde(rename_all = "camelCase")]
134147
#[non_exhaustive]
@@ -140,12 +153,16 @@ pub struct ClosedPosition {
140153
/// The market condition ID (unique market identifier).
141154
pub condition_id: B256,
142155
/// Average entry price for the position.
156+
#[serde_as(as = "DecimalFromAny")]
143157
pub avg_price: Decimal,
144158
/// Total amount bought (cumulative).
159+
#[serde_as(as = "DecimalFromAny")]
145160
pub total_bought: Decimal,
146161
/// Realized profit/loss from the closed position.
162+
#[serde_as(as = "DecimalFromAny")]
147163
pub realized_pnl: Decimal,
148164
/// Final market price when position was closed.
165+
#[serde_as(as = "DecimalFromAny")]
149166
pub cur_price: Decimal,
150167
/// Unix timestamp when the position was closed.
151168
pub timestamp: i64,
@@ -187,8 +204,10 @@ pub struct Trade {
187204
/// The market condition ID (unique market identifier).
188205
pub condition_id: B256,
189206
/// Number of tokens traded.
207+
#[serde_as(as = "DecimalFromAny")]
190208
pub size: Decimal,
191209
/// Execution price per token.
210+
#[serde_as(as = "DecimalFromAny")]
192211
pub price: Decimal,
193212
/// Unix timestamp when the trade occurred.
194213
pub timestamp: i64,

src/gamma/types/response.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use serde_with::NoneAsEmptyString;
1010
use serde_with::json::JsonString;
1111
use serde_with::{DisplayFromStr, StringWithSeparator, formats::CommaSeparator, serde_as};
1212

13-
use crate::serde_helpers::StringFromAny;
13+
use crate::serde_helpers::{DecimalFromAny, StringFromAny};
1414
use crate::types::{Address, B256, Decimal, U256};
1515

1616
/// Image optimization metadata.
@@ -245,8 +245,11 @@ pub struct Event {
245245
pub new: Option<bool>,
246246
pub featured: Option<bool>,
247247
pub restricted: Option<bool>,
248+
#[serde_as(as = "Option<DecimalFromAny>")]
248249
pub liquidity: Option<Decimal>,
250+
#[serde_as(as = "Option<DecimalFromAny>")]
249251
pub volume: Option<Decimal>,
252+
#[serde_as(as = "Option<DecimalFromAny>")]
250253
pub open_interest: Option<Decimal>,
251254
pub sort_by: Option<String>,
252255
pub category: Option<String>,
@@ -260,10 +263,15 @@ pub struct Event {
260263
pub created_at: Option<DateTime<Utc>>,
261264
pub updated_at: Option<DateTime<Utc>>,
262265
pub comments_enabled: Option<bool>,
266+
#[serde_as(as = "Option<DecimalFromAny>")]
263267
pub competitive: Option<Decimal>,
268+
#[serde_as(as = "Option<DecimalFromAny>")]
264269
pub volume_24hr: Option<Decimal>,
270+
#[serde_as(as = "Option<DecimalFromAny>")]
265271
pub volume_1wk: Option<Decimal>,
272+
#[serde_as(as = "Option<DecimalFromAny>")]
266273
pub volume_1mo: Option<Decimal>,
274+
#[serde_as(as = "Option<DecimalFromAny>")]
267275
pub volume_1yr: Option<Decimal>,
268276
pub featured_image: Option<String>,
269277
pub disqus_thread: Option<String>,
@@ -274,7 +282,9 @@ pub struct Event {
274282
#[serde_as(as = "Option<StringFromAny>")]
275283
pub turn_provider_id: Option<String>,
276284
pub enable_order_book: Option<bool>,
285+
#[serde_as(as = "Option<DecimalFromAny>")]
277286
pub liquidity_amm: Option<Decimal>,
287+
#[serde_as(as = "Option<DecimalFromAny>")]
278288
pub liquidity_clob: Option<Decimal>,
279289
pub neg_risk: Option<bool>,
280290
#[serde_as(as = "NoneAsEmptyString")]
@@ -317,7 +327,9 @@ pub struct Event {
317327
pub cant_estimate: Option<bool>,
318328
pub estimated_value: Option<String>,
319329
pub templates: Option<Vec<Template>>,
330+
#[serde_as(as = "Option<DecimalFromAny>")]
320331
pub spreads_main_line: Option<Decimal>,
332+
#[serde_as(as = "Option<DecimalFromAny>")]
321333
pub totals_main_line: Option<Decimal>,
322334
pub carousel_map: Option<String>,
323335
pub pending_deployment: Option<bool>,
@@ -353,13 +365,15 @@ pub struct Market {
353365
pub end_date: Option<DateTime<Utc>>,
354366
pub category: Option<String>,
355367
pub amm_type: Option<String>,
368+
#[serde_as(as = "Option<DecimalFromAny>")]
356369
pub liquidity: Option<Decimal>,
357370
pub sponsor_name: Option<String>,
358371
pub sponsor_image: Option<String>,
359372
pub start_date: Option<DateTime<Utc>>,
360373
pub x_axis_value: Option<String>,
361374
pub y_axis_value: Option<String>,
362375
pub denomination_token: Option<U256>,
376+
#[serde_as(as = "Option<DecimalFromAny>")]
363377
pub fee: Option<Decimal>,
364378
pub image: Option<String>,
365379
pub icon: Option<String>,
@@ -370,6 +384,7 @@ pub struct Market {
370384
pub outcomes: Option<Vec<String>>,
371385
#[serde_as(as = "Option<JsonString>")]
372386
pub outcome_prices: Option<Vec<Decimal>>,
387+
#[serde_as(as = "Option<DecimalFromAny>")]
373388
pub volume: Option<Decimal>,
374389
pub active: Option<bool>,
375390
pub market_type: Option<String>,
@@ -400,21 +415,29 @@ pub struct Market {
400415
pub question_id: Option<B256>,
401416
pub uma_end_date: Option<String>,
402417
pub enable_order_book: Option<bool>,
418+
#[serde_as(as = "Option<DecimalFromAny>")]
403419
pub order_price_min_tick_size: Option<Decimal>,
420+
#[serde_as(as = "Option<DecimalFromAny>")]
404421
pub order_min_size: Option<Decimal>,
405422
pub uma_resolution_status: Option<String>,
406423
pub curation_order: Option<i32>,
424+
#[serde_as(as = "Option<DecimalFromAny>")]
407425
pub volume_num: Option<Decimal>,
426+
#[serde_as(as = "Option<DecimalFromAny>")]
408427
pub liquidity_num: Option<Decimal>,
409428
pub end_date_iso: Option<NaiveDate>,
410429
pub start_date_iso: Option<NaiveDate>,
411430
pub uma_end_date_iso: Option<NaiveDate>,
412431
pub has_reviewed_dates: Option<bool>,
413432
pub ready_for_cron: Option<bool>,
414433
pub comments_enabled: Option<bool>,
434+
#[serde_as(as = "Option<DecimalFromAny>")]
415435
pub volume_24hr: Option<Decimal>,
436+
#[serde_as(as = "Option<DecimalFromAny>")]
416437
pub volume_1wk: Option<Decimal>,
438+
#[serde_as(as = "Option<DecimalFromAny>")]
417439
pub volume_1mo: Option<Decimal>,
440+
#[serde_as(as = "Option<DecimalFromAny>")]
418441
pub volume_1yr: Option<Decimal>,
419442
pub game_start_time: Option<String>,
420443
pub seconds_delay: Option<i32>,
@@ -427,19 +450,32 @@ pub struct Market {
427450
#[serde(rename = "teamBID")]
428451
pub team_b_id: Option<String>,
429452
pub uma_bond: Option<String>,
453+
#[serde_as(as = "Option<DecimalFromAny>")]
430454
pub uma_reward: Option<Decimal>,
431455
pub fpmm_live: Option<bool>,
456+
#[serde_as(as = "Option<DecimalFromAny>")]
432457
pub volume_24hr_amm: Option<Decimal>,
458+
#[serde_as(as = "Option<DecimalFromAny>")]
433459
pub volume_1wk_amm: Option<Decimal>,
460+
#[serde_as(as = "Option<DecimalFromAny>")]
434461
pub volume_1mo_amm: Option<Decimal>,
462+
#[serde_as(as = "Option<DecimalFromAny>")]
435463
pub volume_1yr_amm: Option<Decimal>,
464+
#[serde_as(as = "Option<DecimalFromAny>")]
436465
pub volume_24hr_clob: Option<Decimal>,
466+
#[serde_as(as = "Option<DecimalFromAny>")]
437467
pub volume_1wk_clob: Option<Decimal>,
468+
#[serde_as(as = "Option<DecimalFromAny>")]
438469
pub volume_1mo_clob: Option<Decimal>,
470+
#[serde_as(as = "Option<DecimalFromAny>")]
439471
pub volume_1yr_clob: Option<Decimal>,
472+
#[serde_as(as = "Option<DecimalFromAny>")]
440473
pub volume_amm: Option<Decimal>,
474+
#[serde_as(as = "Option<DecimalFromAny>")]
441475
pub volume_clob: Option<Decimal>,
476+
#[serde_as(as = "Option<DecimalFromAny>")]
442477
pub liquidity_amm: Option<Decimal>,
478+
#[serde_as(as = "Option<DecimalFromAny>")]
443479
pub liquidity_clob: Option<Decimal>,
444480
pub maker_base_fee: Option<i32>,
445481
pub taker_base_fee: Option<i32>,
@@ -460,18 +496,30 @@ pub struct Market {
460496
pub ready_timestamp: Option<DateTime<Utc>>,
461497
pub funded_timestamp: Option<DateTime<Utc>>,
462498
pub accepting_orders_timestamp: Option<DateTime<Utc>>,
499+
#[serde_as(as = "Option<DecimalFromAny>")]
463500
pub competitive: Option<Decimal>,
501+
#[serde_as(as = "Option<DecimalFromAny>")]
464502
pub rewards_min_size: Option<Decimal>,
503+
#[serde_as(as = "Option<DecimalFromAny>")]
465504
pub rewards_max_spread: Option<Decimal>,
505+
#[serde_as(as = "Option<DecimalFromAny>")]
466506
pub spread: Option<Decimal>,
467507
pub automatically_resolved: Option<bool>,
508+
#[serde_as(as = "Option<DecimalFromAny>")]
468509
pub one_day_price_change: Option<Decimal>,
510+
#[serde_as(as = "Option<DecimalFromAny>")]
469511
pub one_hour_price_change: Option<Decimal>,
512+
#[serde_as(as = "Option<DecimalFromAny>")]
470513
pub one_week_price_change: Option<Decimal>,
514+
#[serde_as(as = "Option<DecimalFromAny>")]
471515
pub one_month_price_change: Option<Decimal>,
516+
#[serde_as(as = "Option<DecimalFromAny>")]
472517
pub one_year_price_change: Option<Decimal>,
518+
#[serde_as(as = "Option<DecimalFromAny>")]
473519
pub last_trade_price: Option<Decimal>,
520+
#[serde_as(as = "Option<DecimalFromAny>")]
474521
pub best_bid: Option<Decimal>,
522+
#[serde_as(as = "Option<DecimalFromAny>")]
475523
pub best_ask: Option<Decimal>,
476524
pub automatically_active: Option<bool>,
477525
pub clear_book_on_start: Option<bool>,
@@ -484,6 +532,7 @@ pub struct Market {
484532
pub game_id: Option<String>,
485533
pub group_item_range: Option<String>,
486534
pub sports_market_type: Option<String>,
535+
#[serde_as(as = "Option<DecimalFromAny>")]
487536
pub line: Option<Decimal>,
488537
pub uma_resolution_statuses: Option<String>,
489538
pub pending_deployment: Option<bool>,
@@ -532,11 +581,14 @@ pub struct ClobReward {
532581
pub condition_id: Option<B256>,
533582
pub start_date: Option<NaiveDate>,
534583
pub end_date: Option<NaiveDate>,
584+
#[serde_as(as = "Option<DecimalFromAny>")]
535585
pub rewards_amount: Option<Decimal>,
586+
#[serde_as(as = "Option<DecimalFromAny>")]
536587
pub rewards_daily_rate: Option<Decimal>,
537588
}
538589

539590
/// A series of related events.
591+
#[serde_as]
540592
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Builder)]
541593
#[serde(rename_all = "camelCase")]
542594
#[non_exhaustive]
@@ -566,9 +618,13 @@ pub struct Series {
566618
pub created_at: Option<DateTime<Utc>>,
567619
pub updated_at: Option<DateTime<Utc>>,
568620
pub comments_enabled: Option<bool>,
621+
#[serde_as(as = "Option<DecimalFromAny>")]
569622
pub competitive: Option<Decimal>,
623+
#[serde_as(as = "Option<DecimalFromAny>")]
570624
pub volume_24hr: Option<Decimal>,
625+
#[serde_as(as = "Option<DecimalFromAny>")]
571626
pub volume: Option<Decimal>,
627+
#[serde_as(as = "Option<DecimalFromAny>")]
572628
pub liquidity: Option<Decimal>,
573629
pub start_date: Option<DateTime<Utc>>,
574630
#[serde(rename = "pythTokenID")]
@@ -585,11 +641,13 @@ pub struct Series {
585641
}
586642

587643
/// A comment position.
644+
#[serde_as]
588645
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Builder)]
589646
#[serde(rename_all = "camelCase")]
590647
#[non_exhaustive]
591648
pub struct CommentPosition {
592649
pub token_id: Option<U256>,
650+
#[serde_as(as = "Option<DecimalFromAny>")]
593651
pub position_size: Option<Decimal>,
594652
}
595653

0 commit comments

Comments
 (0)