Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions a2a-pb/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut builder = pbjson_build::Builder::new();
builder.out_dir(&out_dir);
builder.register_descriptors(&descriptor_set)?;
// Spec §5.7: implementations SHOULD ignore unrecognized fields in
// messages, allowing for forward compatibility. Unknown enum string
// values are still rejected.
builder.ignore_unknown_fields();
builder.build(PROTOJSON_PACKAGES)?;
patch_generated_protojson_serde(&out_dir)?;

Expand Down
40 changes: 40 additions & 0 deletions a2a-pb/tests/protojson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,46 @@ fn agent_card_protojson_deserializes_null_skills_as_empty() {
assert_eq!(parsed, expected);
}

/// Spec §5.7: implementations SHOULD ignore unrecognized fields in messages,
/// allowing for forward compatibility. Unknown fields must be tolerated both
/// at the top level and inside nested messages.
#[test]
fn task_protojson_decode_ignores_unknown_fields() {
let json = json!({
"id": "task-1",
"contextId": "ctx-1",
"status": {
"state": "TASK_STATE_COMPLETED",
"futureStatusField": "ignored"
},
"futureTopLevelField": {"nested": ["values", 1, true]}
});

let task = a2a_pb::protojson_conv::from_value::<a2a::Task>(json)
.expect("unknown fields must be ignored");
assert_eq!(task.id, "task-1");
assert_eq!(task.context_id, "ctx-1");
assert_eq!(task.status.state, a2a::TaskState::Completed);
}

/// Unknown *enum string values* are still rejected: only unrecognized fields
/// are tolerated (`ignore_unknown_enum_variants` is intentionally not
/// enabled).
#[test]
fn task_protojson_decode_rejects_unknown_enum_values() {
let json = json!({
"id": "task-1",
"contextId": "ctx-1",
"status": {"state": "TASK_STATE_FROM_THE_FUTURE"}
});

let result = a2a_pb::protojson_conv::from_value::<a2a::Task>(json);
assert!(
result.is_err(),
"unknown enum values must still be rejected"
);
}

#[test]
fn stream_response_protojson_roundtrip_serializes_oneof_payloads() {
let response = StreamResponse {
Expand Down
27 changes: 26 additions & 1 deletion a2a-server/src/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,37 @@ mod tests {
#[tokio::test]
async fn test_invalid_params() {
let app = make_app();
let params = serde_json::json!({"bogus": true});
// Type mismatch: `message` must be an object. A merely-unknown field
// is not invalid params; it is ignored per spec §5.7.
let params = serde_json::json!({"message": "not-an-object"});
let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
assert!(resp.error.is_some());
assert_eq!(resp.error.unwrap().code, error_code::PARSE_ERROR);
}

/// Spec §5.7: unrecognized fields in request params are ignored for
/// forward compatibility instead of being rejected.
#[tokio::test]
async fn test_unknown_params_fields_are_ignored() {
let app = make_app();
let params = serde_json::json!({
"message": {
"messageId": "m1",
"role": "ROLE_USER",
"parts": [{"text": "hi"}],
"futureField": true
},
"anotherFutureField": {"nested": 1}
});
let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
assert!(
resp.error.is_none(),
"unknown fields must be ignored: {:?}",
resp.error
);
assert!(resp.result.is_some());
}

#[tokio::test]
async fn test_list_tasks() {
let app = make_app();
Expand Down
Loading