Skip to content

Commit 09e971d

Browse files
committed
Make List Deserialization Fall Back to Defaults
Wrap list deserializers with `DefaultOnError` so invalid outer shapes default instead of failing, while still skipping invalid elements inside valid arrays.
1 parent 1a873bb commit 09e971d

7 files changed

Lines changed: 127 additions & 30 deletions

File tree

src/agent.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub struct InitializeResponse {
128128
#[serde(default)]
129129
pub agent_capabilities: AgentCapabilities,
130130
/// Authentication methods supported by the agent.
131-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
131+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
132132
#[serde(default)]
133133
pub auth_methods: Vec<AuthMethod>,
134134
/// Information about the Agent name and version sent to the Client.
@@ -1024,7 +1024,7 @@ pub struct NewSessionResponse {
10241024
#[serde(default)]
10251025
pub models: Option<SessionModelState>,
10261026
/// Initial session configuration options if supported by the Agent.
1027-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1027+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
10281028
#[serde(default)]
10291029
pub config_options: Option<Vec<SessionConfigOption>>,
10301030
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1200,7 +1200,7 @@ pub struct LoadSessionResponse {
12001200
#[serde(default)]
12011201
pub models: Option<SessionModelState>,
12021202
/// Initial session configuration options if supported by the Agent.
1203-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1203+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
12041204
#[serde(default)]
12051205
pub config_options: Option<Vec<SessionConfigOption>>,
12061206
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1384,7 +1384,7 @@ pub struct ForkSessionResponse {
13841384
#[serde(default)]
13851385
pub models: Option<SessionModelState>,
13861386
/// Initial session configuration options if supported by the Agent.
1387-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1387+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
13881388
#[serde(default)]
13891389
pub config_options: Option<Vec<SessionConfigOption>>,
13901390
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1574,7 +1574,7 @@ pub struct ResumeSessionResponse {
15741574
#[serde(default)]
15751575
pub models: Option<SessionModelState>,
15761576
/// Initial session configuration options if supported by the Agent.
1577-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1577+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
15781578
#[serde(default)]
15791579
pub config_options: Option<Vec<SessionConfigOption>>,
15801580
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1817,7 +1817,7 @@ impl ListSessionsRequest {
18171817
#[non_exhaustive]
18181818
pub struct ListSessionsResponse {
18191819
/// Array of session information objects
1820-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
1820+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
18211821
pub sessions: Vec<SessionInfo>,
18221822
/// Opaque cursor token. If present, pass this in the next request's cursor parameter
18231823
/// to fetch the next page. If absent, there are no more results.
@@ -1962,7 +1962,7 @@ pub struct SessionModeState {
19621962
/// The current mode the Agent is in.
19631963
pub current_mode_id: SessionModeId,
19641964
/// The set of modes that the Agent can operate in
1965-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
1965+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
19661966
pub available_modes: Vec<SessionMode>,
19671967
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
19681968
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -2663,7 +2663,7 @@ impl SetSessionConfigOptionRequest {
26632663
#[non_exhaustive]
26642664
pub struct SetSessionConfigOptionResponse {
26652665
/// The full set of configuration options and their current values.
2666-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
2666+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
26672667
pub config_options: Vec<SessionConfigOption>,
26682668
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
26692669
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -3255,7 +3255,7 @@ pub struct SessionModelState {
32553255
/// The current model the Agent is in.
32563256
pub current_model_id: ModelId,
32573257
/// The set of models that the Agent can use
3258-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
3258+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
32593259
pub available_models: Vec<ModelInfo>,
32603260
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
32613261
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -3531,7 +3531,7 @@ pub struct ProviderInfo {
35313531
/// Provider identifier, for example "main" or "openai".
35323532
pub id: String,
35333533
/// Supported protocol types for this provider.
3534-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
3534+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
35353535
pub supported: Vec<LlmProtocol>,
35363536
/// Whether this provider is mandatory and cannot be disabled via `providers/disable`.
35373537
/// If true, clients must not call `providers/disable` for this id.
@@ -3632,7 +3632,7 @@ impl ListProvidersRequest {
36323632
#[non_exhaustive]
36333633
pub struct ListProvidersResponse {
36343634
/// Configurable providers with current routing info suitable for UI display.
3635-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
3635+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
36363636
pub providers: Vec<ProviderInfo>,
36373637
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
36383638
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at

src/client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ impl CurrentModeUpdate {
162162
#[non_exhaustive]
163163
pub struct ConfigOptionUpdate {
164164
/// The full set of configuration options and their current values.
165-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
165+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
166166
pub config_options: Vec<SessionConfigOption>,
167167
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
168168
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -412,7 +412,7 @@ impl ContentChunk {
412412
#[non_exhaustive]
413413
pub struct AvailableCommandsUpdate {
414414
/// Commands the agent can execute
415-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
415+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
416416
pub available_commands: Vec<AvailableCommand>,
417417
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
418418
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -1554,7 +1554,7 @@ pub struct ClientCapabilities {
15541554
///
15551555
/// The position encodings supported by the client, in order of preference.
15561556
#[cfg(feature = "unstable_nes")]
1557-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
1557+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
15581558
#[serde(default, skip_serializing_if = "Vec::is_empty")]
15591559
pub position_encodings: Vec<PositionEncodingKind>,
15601560

src/content.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ impl ResourceLink {
459459
#[serde(rename_all = "camelCase")]
460460
#[non_exhaustive]
461461
pub struct Annotations {
462-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
462+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
463463
#[serde(default)]
464464
pub audience: Option<Vec<Role>>,
465465
pub last_modified: Option<String>,

src/nes.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ pub struct StartNesRequest {
10991099
/// The root URI of the workspace.
11001100
pub workspace_uri: Option<String>,
11011101
/// The workspace folders.
1102-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1102+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
11031103
#[serde(default)]
11041104
pub workspace_folders: Option<Vec<WorkspaceFolder>>,
11051105
/// Repository metadata, if the workspace is a git repository.
@@ -1436,27 +1436,27 @@ impl SuggestNesRequest {
14361436
#[non_exhaustive]
14371437
pub struct NesSuggestContext {
14381438
/// Recently accessed files.
1439-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1439+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14401440
#[serde(default)]
14411441
pub recent_files: Option<Vec<NesRecentFile>>,
14421442
/// Related code snippets.
1443-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1443+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14441444
#[serde(default)]
14451445
pub related_snippets: Option<Vec<NesRelatedSnippet>>,
14461446
/// Recent edit history.
1447-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1447+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14481448
#[serde(default)]
14491449
pub edit_history: Option<Vec<NesEditHistoryEntry>>,
14501450
/// Recent user actions (typing, navigation, etc.).
1451-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1451+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14521452
#[serde(default)]
14531453
pub user_actions: Option<Vec<NesUserAction>>,
14541454
/// Currently open files in the editor.
1455-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1455+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14561456
#[serde(default)]
14571457
pub open_files: Option<Vec<NesOpenFile>>,
14581458
/// Current diagnostics (errors, warnings).
1459-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
1459+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
14601460
#[serde(default)]
14611461
pub diagnostics: Option<Vec<NesDiagnostic>>,
14621462
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1757,7 +1757,7 @@ pub enum NesDiagnosticSeverity {
17571757
#[non_exhaustive]
17581758
pub struct SuggestNesResponse {
17591759
/// The list of suggestions.
1760-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
1760+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
17611761
pub suggestions: Vec<NesSuggestion>,
17621762
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
17631763
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at

src/plan.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
use schemars::JsonSchema;
99
use serde::{Deserialize, Serialize};
10-
use serde_with::{VecSkipError, serde_as, skip_serializing_none};
10+
use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
1111

1212
use crate::{IntoOption, Meta, SkipListener};
1313

@@ -28,7 +28,7 @@ pub struct Plan {
2828
///
2929
/// When updating a plan, the agent must send a complete list of all entries
3030
/// with their current status. The client replaces the entire plan with each update.
31-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
31+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
3232
pub entries: Vec<PlanEntry>,
3333
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
3434
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at

src/serde_util.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ mod skip_listener_tests {
6363

6464
use serde::{Deserialize, Serialize};
6565
use serde_json::json;
66-
use serde_with::{VecSkipError, serde_as};
66+
use serde_with::{DefaultOnError, VecSkipError, serde_as};
6767

6868
thread_local! {
6969
static SKIP_COUNT: Cell<u32> = const { Cell::new(0) };
@@ -95,6 +95,103 @@ mod skip_listener_tests {
9595
assert_eq!(wrapper.values, vec![1, 2, 3]);
9696
assert_eq!(SKIP_COUNT.with(Cell::get), 2);
9797
}
98+
99+
/// Mirrors the pattern applied to every required `Vec<T>` field in the
100+
/// protocol: `DefaultOnError<VecSkipError<_, ...>>` + `#[serde(default)]`.
101+
/// Element-level failures are skipped; any outer shape error (`null`, a
102+
/// string, a map, etc.) collapses to `Default::default()` (i.e. `vec![]`).
103+
#[serde_as]
104+
#[derive(Deserialize, Debug, PartialEq)]
105+
struct ResilientVec {
106+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, CountingListener>>")]
107+
#[serde(default)]
108+
values: Vec<u32>,
109+
}
110+
111+
#[test]
112+
fn resilient_vec_tolerates_missing_null_and_wrong_type() {
113+
// Missing field -> `#[serde(default)]` supplies `vec![]`.
114+
let r: ResilientVec = serde_json::from_value(json!({})).unwrap();
115+
assert_eq!(r.values, Vec::<u32>::new());
116+
117+
// Explicit null -> `DefaultOnError` swallows the type error.
118+
let r: ResilientVec = serde_json::from_value(json!({"values": null})).unwrap();
119+
assert_eq!(r.values, Vec::<u32>::new());
120+
121+
// Wrong outer type (string) -> `DefaultOnError` swallows.
122+
let r: ResilientVec = serde_json::from_value(json!({"values": "oops"})).unwrap();
123+
assert_eq!(r.values, Vec::<u32>::new());
124+
125+
// Wrong outer type (object) -> `DefaultOnError` swallows.
126+
let r: ResilientVec = serde_json::from_value(json!({"values": {"k": 1}})).unwrap();
127+
assert_eq!(r.values, Vec::<u32>::new());
128+
129+
// Valid array with element errors -> `VecSkipError` skips per-element.
130+
SKIP_COUNT.with(|c| c.set(0));
131+
let r: ResilientVec =
132+
serde_json::from_value(json!({"values": [1, "oops", 2, {}, 3]})).unwrap();
133+
assert_eq!(r.values, vec![1, 2, 3]);
134+
assert_eq!(SKIP_COUNT.with(Cell::get), 2);
135+
}
136+
137+
#[test]
138+
fn resilient_vec_does_not_invoke_inspector_on_outer_failure() {
139+
SKIP_COUNT.with(|c| c.set(0));
140+
141+
// Outer failures are swallowed silently by `DefaultOnError`; the
142+
// inspector only sees per-element failures inside a valid array.
143+
let _r: ResilientVec = serde_json::from_value(json!({"values": null})).unwrap();
144+
let _r: ResilientVec = serde_json::from_value(json!({"values": "oops"})).unwrap();
145+
let _r: ResilientVec = serde_json::from_value(json!({"values": {}})).unwrap();
146+
147+
assert_eq!(SKIP_COUNT.with(Cell::get), 0);
148+
}
149+
150+
/// Mirrors the pattern applied to every optional `Option<Vec<T>>` field:
151+
/// `DefaultOnError<Option<VecSkipError<_, ...>>>` + `#[serde(default)]`.
152+
/// `null` becomes `None`; outer shape errors also collapse to `None`;
153+
/// element-level failures are skipped inside the array.
154+
#[serde_as]
155+
#[derive(Deserialize, Debug, PartialEq)]
156+
struct ResilientOptionVec {
157+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, CountingListener>>>")]
158+
#[serde(default)]
159+
values: Option<Vec<u32>>,
160+
}
161+
162+
#[test]
163+
fn resilient_option_vec_tolerates_missing_null_and_wrong_type() {
164+
// Missing field -> `None`.
165+
let r: ResilientOptionVec = serde_json::from_value(json!({})).unwrap();
166+
assert_eq!(r.values, None);
167+
168+
// Explicit null -> `None`.
169+
let r: ResilientOptionVec = serde_json::from_value(json!({"values": null})).unwrap();
170+
assert_eq!(r.values, None);
171+
172+
// Empty array -> `Some(vec![])`.
173+
let r: ResilientOptionVec = serde_json::from_value(json!({"values": []})).unwrap();
174+
assert_eq!(r.values, Some(Vec::<u32>::new()));
175+
176+
// Valid array -> `Some(vec)`.
177+
let r: ResilientOptionVec = serde_json::from_value(json!({"values": [1, 2, 3]})).unwrap();
178+
assert_eq!(r.values, Some(vec![1, 2, 3]));
179+
180+
// Wrong outer type (string) -> `DefaultOnError` collapses to `None`.
181+
let r: ResilientOptionVec = serde_json::from_value(json!({"values": "oops"})).unwrap();
182+
assert_eq!(r.values, None);
183+
184+
// Wrong outer type (object) -> `DefaultOnError` collapses to `None`.
185+
let r: ResilientOptionVec = serde_json::from_value(json!({"values": {"k": 1}})).unwrap();
186+
assert_eq!(r.values, None);
187+
188+
// Valid array with element errors -> `VecSkipError` skips per-element.
189+
SKIP_COUNT.with(|c| c.set(0));
190+
let r: ResilientOptionVec =
191+
serde_json::from_value(json!({"values": [1, "oops", 2, {}, 3]})).unwrap();
192+
assert_eq!(r.values, Some(vec![1, 2, 3]));
193+
assert_eq!(SKIP_COUNT.with(Cell::get), 2);
194+
}
98195
}
99196

100197
// ---- IntoOption ----

src/tool_call.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ pub struct ToolCall {
3737
#[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
3838
pub status: ToolCallStatus,
3939
/// Content produced by the tool call.
40-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
40+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
4141
#[serde(default, skip_serializing_if = "Vec::is_empty")]
4242
pub content: Vec<ToolCallContent>,
4343
/// File locations affected by this tool call.
4444
/// Enables "follow-along" features in clients.
45-
#[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
45+
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
4646
#[serde(default, skip_serializing_if = "Vec::is_empty")]
4747
pub locations: Vec<ToolCallLocation>,
4848
/// Raw input parameters sent to the tool.
@@ -226,11 +226,11 @@ pub struct ToolCallUpdateFields {
226226
/// Update the human-readable title.
227227
pub title: Option<String>,
228228
/// Replace the content collection.
229-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
229+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
230230
#[serde(default)]
231231
pub content: Option<Vec<ToolCallContent>>,
232232
/// Replace the locations collection.
233-
#[serde_as(deserialize_as = "Option<VecSkipError<_, SkipListener>>")]
233+
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
234234
#[serde(default)]
235235
pub locations: Option<Vec<ToolCallLocation>>,
236236
/// Update the raw input.

0 commit comments

Comments
 (0)