Skip to content

Float fields fail to deserialize when serde_json's arbitrary_precision is enabled elsewhere in the build #1299

Description

@jmrplens

When any crate in a downstream build enables serde_json/arbitrary_precision, Cargo feature unification turns it on for rmcp as well, and every f32/f64 field that rmcp reads through serde's buffered path fails on a decimal with invalid type: map, expected f32. Integers still parse, so only fractional values are affected. Because ServerResult and ServerNotification are untagged unions ending in a catch-all, nothing reports the error: a tools/call result silently becomes CustomResult and a fractional notifications/progress becomes CustomNotification.

This is serde's known limitation with that feature (serde-rs/json#721, serde-rs/serde#1183): serde replays a buffered decimal as serde_json's private number map, which a plain float field cannot read. rmcp cannot keep the feature out of a downstream build, and the case is real: the OpenAI Codex CLI gets it from starlark 0.14.2 (unconditionally) and from one of its own crates, and as a result every tool call to a server that sets a fractional annotations.priority fails there (openai/codex#38979).

Reproduction

[dependencies]
rmcp = { version = "=3.4.1", default-features = false, features = ["client"] }
serde_json = "1"

[features]
ap = ["serde_json/arbitrary_precision"]
use rmcp::model::{JsonRpcMessage, ServerResult};
use rmcp::service::{RoleClient, RxJsonRpcMessage};

let line = r#"{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ok","annotations":{"priority":0.6}}]}}"#;
let msg: RxJsonRpcMessage<RoleClient> = serde_json::from_str(line).unwrap();
let JsonRpcMessage::Response(resp) = msg else { unreachable!() };
println!("{}", matches!(resp.result, ServerResult::CallToolResult(_)));
Input without ap with ap
tools/call result, priority: 0.6 CallToolResult CustomResult
tools/call result, priority: 1.0 CallToolResult CustomResult
tools/call result, priority: 1 CallToolResult CallToolResult
CallToolResult parsed directly, priority: 0.6 Ok Err(invalid type: map, expected f32)
notifications/progress, progress: 0.5 ProgressNotification CustomNotification

The same happens on 3.2.0. It also reaches rmcp talking to itself: an f64 progress of 1 serializes as 1.0, so in such a build an rmcp client drops the progress notifications an rmcp server sends. Running the crate's own tests with the feature on, test_request_timeout_progress (two tests) and stateless_server_first_request_handler_can_send_notifications fail on main for that reason.

Affected fields (main at 6e47ca3)

  • Annotations.priority (model/annotated.rs)
  • ProgressNotificationParam.progress, total (model.rs)
  • CreateMessageRequestParams.temperature (model.rs)
  • ModelPreferences.cost_priority, speed_priority, intelligence_priority (model.rs)
  • NumberSchema.minimum, maximum, default (model/elicitation_schema.rs)

Proposal

Deserialize these fields through serde_json::Number, which understands both representations, with pub(crate) helpers in model/serde_impl.rs and #[serde(deserialize_with = ...)] (plus default on the Option fields). Serialization, the generated schemas and the public API do not change, and without the feature the decoded values are the same as today, since serde_json already reads an f32 field as an f64 and casts it. With the feature on, every row above decodes as CallToolResult or ProgressNotification, including spellings such as 0.60 and 6e-1, 0.6 decodes to exactly 0.6f32, and a string priority is still rejected.

I am opening a pull request with this change right after this issue, covering all ten fields. The regression tests need a build where serde_json has the feature, so it enables it for rmcp's tests through a dev-dependency; the pull request describes the trade-off and the alternative (a small crate outside the workspace with a CI step of its own), and I am happy to switch if you prefer that.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High: significant functionality gap or spec violationT-modelModel/data structure changesbugSomething is not working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions