fix(serde): accept JSON numbers as Decimal in CLOB/data/gamma responses#328
Open
moeuu wants to merge 2 commits intoPolymarket:mainfrom
Open
fix(serde): accept JSON numbers as Decimal in CLOB/data/gamma responses#328moeuu wants to merge 2 commits intoPolymarket:mainfrom
moeuu wants to merge 2 commits intoPolymarket:mainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #328 +/- ##
==========================================
+ Coverage 85.54% 85.60% +0.05%
==========================================
Files 32 32
Lines 5167 5251 +84
==========================================
+ Hits 4420 4495 +75
- Misses 747 756 +9
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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>
d2ed1b1 to
96410cf
Compare
Adds a decimal_from_any_tests module alongside the existing serde_helpers tests. Covers: - JSON float (0.01) — the /tick-size regression - JSON float without precision loss (0.1 must round-trip through the shortest-string representation, not 0.1000000000000000055...) - JSON unsigned integer (42) - JSON signed integer (-7) - String-encoded Decimal (backwards compat path) - Non-numeric string (must error) - Null on a required field (must error) - Option<DecimalFromAny> accepting null / float / absent field - A full "minimum_tick_size" fixture matching the shape that triggered the original bug against the live /tick-size endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Accept JSON numbers as
Decimalacross CLOB, data, and Gamma response typesSummary
Several Polymarket CLOB / data-api / Gamma responses return numeric fields as JSON
numbers (e.g.
{"minimum_tick_size": 0.01},{"avgPrice": 0.36},{"lastTradePrice": 0.54}) rather than strings. The defaultrust_decimal::Decimaldeserializer inserderejects these with errors like:This blocks every consumer of these structs at deserialization time. In particular,
Client::tick_sizeis called transitively by the order-builder(
limit_order().build()/market_order().build()), so any attempt to place alive order currently fails before the order is signed, and
sync_markets-styleworkflows that iterate the Gamma catalog fail on the first sports / prediction
market with numeric
bestBid/bestAsk/ etc.Reproduction
Against production endpoints (as of the date of this PR):
Live JSON samples observed:
Change
Introduce a reusable
serde_with::DeserializeAs/SerializeAsadapterserde_helpers::DecimalFromAnythat accepts strings, integers, or floats, andapply it to every affected field across CLOB, data, and Gamma response types.
src/serde_helpers.rsNew
DecimalFromAnyadapter. Visits:visit_str/visit_string→Decimal::from_strvisit_i64/visit_u64→Decimal::fromvisit_f64→Decimal::from_str(&v.to_string())(round-tripsthrough the shortest-round-trip
f64→ string conversion to avoid the binaryfloat precision trap, e.g.
0.1→0.1000000000000000055...)On the serialize side it delegates to the existing
Decimal: Serializeimpl sooutput format is unchanged when these types are re-serialized in tests or mock
helpers.
Gated on the same feature set as the existing
StringFromAnyhelper(
bridge | clob | data | gamma).src/clob/types/mod.rsTickSize::Deserializenow delegates toDecimalFromAnyand then passes theDecimalthrough the existingTryFrom<Decimal> for TickSizeconversion. Thevalidation that the value must be one of
{0.1, 0.01, 0.001, 0.0001}ispreserved.
src/data/types/response.rsPosition—#[serde_as(as = "DecimalFromAny")]on all tenDecimalfields (
size,avg_price,initial_value,current_value,cash_pnl,percent_pnl,total_bought,realized_pnl,percent_realized_pnl,cur_price).ClosedPosition— adds#[serde_as]on the struct (was missing) andDecimalFromAnyon its fourDecimalfields.Trade—DecimalFromAnyonsizeandprice.src/gamma/types/response.rsMarket,Event, and related structs:#[serde_as(as = "Option<DecimalFromAny>")]applied to every
Option<Decimal>field (56 fields across the module). Structsthat didn't previously carry the
#[serde_as]proc-macro marker get it added.Covered fields include price / book fields (
best_bid,best_ask,last_trade_price,spread), volume fields (volume,volume_num,volume_24hr,volume_1wk,volume_1mo,volume_1yr, and their_amm/_clobvariants), liquidity fields (
liquidity,liquidity_num,liquidity_amm,liquidity_clob), price-change fields (one_day_price_change,one_hour_price_change,one_week_price_change,one_month_price_change,one_year_price_change), and misc numeric fields (fee,rewards_min_size,rewards_max_spread,competitive,line,uma_reward,order_price_min_tick_size,order_min_size,spreads_main_line,totals_main_line).The
outcome_prices: Option<Vec<Decimal>>field is left unchanged because ituses
#[serde_as(as = "Option<JsonString>")]and is wrapped in a stringifiedJSON array whose inner Decimals are already strings in live responses.
Scope / non-goals
SignedOrder,Order) are untouched;these are still serialized as strings when sent to the API as required.
strings;
DecimalFromAnyhandles both.rust_decimal,serde, andserde_withdependencies.Tests
The existing
httpmock-based tests continue to pass. I can add regressioncoverage in a follow-up that feeds raw JSON-number responses through an
httpmockserver, modeled ontests/clob.rs::tick_size_*andtests/data.rs::positions_*.Context
Surfaced from a downstream consumer wiring Polymarket into a live trading
runtime. The bug currently blocks end-to-end order placement and any
sync_markets-style metadata pull. A minimal local reproduction walks throughClobClient::tick_size → /tick-size → 200 {"minimum_tick_size":0.01}and failsin
TickSize::deserializeinsideDecimal::deserialize.Note
Medium Risk
Touches many response structs and changes how numerous
Decimalfields deserialize, so regressions could surface if any endpoints relied on strict string-only parsing or if float-to-string round-tripping behaves unexpectedly.Overview
Fixes deserialization failures when Polymarket APIs return
Decimal-typed fields as JSON numbers instead of strings.Introduces a reusable
serde_helpers::DecimalFromAnyadapter (with tests) that accepts string/int/float JSON values, and applies it to CLOBTickSizeplus affectedDecimalfields across Data API (Position/ClosedPosition/Trade) and Gamma response types (manyOption<Decimal>market/event metrics like liquidity, volume, prices, and spreads).Reviewed by Cursor Bugbot for commit 350a69a. Bugbot is set up for automated code reviews on this repo. Configure here.