fix: prevent CallToolResult and GetTaskPayloadResult from shadowing CustomResult in untagged enums (#771)

The `#[serde(default)]` on `CallToolResult.content` (added in #752) made
all fields optional, causing `CallToolResult` to greedily match any JSON
object during `#[serde(untagged)]` deserialization of `ServerResult`.
Similarly, `GetTaskPayloadResult(Value)` matched everything before
`CustomResult(Value)` could be reached.

Fix by replacing derived `Deserialize` impls with custom ones:
- `CallToolResult`: require at least one known field to be present
- `GetTaskPayloadResult`: always fail (indistinguishable from
  `CustomResult` in JSON; construct programmatically via `::new()`)
This commit is contained in:
Dale Seo 2026-03-23 15:20:55 -04:00 • committed by GitHub
commit c8c0c0cffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -123,7 +123,7 @@ pub struct GetTaskResult {
/// (e.g., `CallToolResult` for `tools/call`). This is represented as
/// an open object. The payload is the original request's result
/// serialized as a JSON value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct GetTaskPayloadResult(pub Value);
@ -135,6 +135,25 @@ impl GetTaskPayloadResult {
}
}
// Custom Deserialize that always fails, so that `GetTaskPayloadResult` is skipped
// during `#[serde(untagged)]` enum deserialization (e.g. `ServerResult`).
// The payload has the same JSON shape as `CustomResult(Value)`, so they are
// indistinguishable. `CustomResult` acts as the catch-all instead.
// `GetTaskPayloadResult` should be constructed programmatically via `::new()`.
impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Consume the value so the deserializer state stays consistent.
serde::de::IgnoredAny::deserialize(deserializer)?;
Err(serde::de::Error::custom(
"GetTaskPayloadResult cannot be deserialized directly; \
use CustomResult as the catch-all",
))
}
}
/// Response to a `tasks/cancel` request.
///
/// Per spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.