fix(elicitation): preserve enumNames through ElicitationSchema serde round-trip (#905)
* fix(elicitation): preserve enumNames through ElicitationSchema serde round-trip UntitledSingleSelectEnumSchema lacked deny_unknown_fields, so a legacy enum payload containing enumNames was silently matched by that variant (ignoring the field) rather than falling through to LegacyEnumSchema. The enumNames array was lost on re-serialization. Add deny_unknown_fields to UntitledSingleSelectEnumSchema so that any unknown field (including enumNames) causes serde to try the next untagged variant, reaching LegacyEnumSchema correctly. Also add skip_serializing_if = "Option::is_none" to LegacyEnumSchema::enum_names so that an untitled legacy enum without enumNames does not serialize "enumNames": null. Fixes #903 * test(elicitation): regenerate server schema snapshot for deny_unknown_fields deny_unknown_fields on UntitledSingleSelectEnumSchema makes schemars emit additionalProperties: false for that definition. Regenerate the golden schema fixtures to match (UPDATE_SCHEMA=1 cargo test -p rmcp --test test_message_schema --all-features).
This commit is contained in:
parent
bf71eb8b09
commit
4fd4986b62
3 changed files with 43 additions and 1 deletions
|
|
@ -544,12 +544,13 @@ pub struct LegacyEnumSchema {
|
||||||
pub description: Option<Cow<'static, str>>,
|
pub description: Option<Cow<'static, str>>,
|
||||||
#[serde(rename = "enum")]
|
#[serde(rename = "enum")]
|
||||||
pub enum_: Vec<String>,
|
pub enum_: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub enum_names: Option<Vec<String>>,
|
pub enum_names: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Untitled single-select
|
/// Untitled single-select
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct UntitledSingleSelectEnumSchema {
|
pub struct UntitledSingleSelectEnumSchema {
|
||||||
|
|
@ -1742,6 +1743,45 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_enum_schema_roundtrip_preserves_enum_names() -> anyhow::Result<()> {
|
||||||
|
// Regression test for: legacy enum payload with `enumNames` was silently
|
||||||
|
// deserialized as `UntitledSingleSelectEnumSchema` (which has no `enumNames`
|
||||||
|
// field), causing the array to be dropped on re-serialization.
|
||||||
|
let input = serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"choice": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["opt1", "opt2", "opt3"],
|
||||||
|
"enumNames": ["Option One", "Option Two", "Option Three"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let schema: ElicitationSchema = serde_json::from_value(input.clone())?;
|
||||||
|
let output = serde_json::to_value(&schema)?;
|
||||||
|
assert_eq!(
|
||||||
|
output["properties"]["choice"]["enumNames"],
|
||||||
|
serde_json::json!(["Option One", "Option Two", "Option Three"]),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_enum_schema_no_enum_names_omits_field() -> anyhow::Result<()> {
|
||||||
|
// `LegacyEnumSchema` with `enum_names: None` must not serialize `"enumNames": null`.
|
||||||
|
let schema = EnumSchema::Legacy(LegacyEnumSchema {
|
||||||
|
type_: StringTypeConst,
|
||||||
|
title: None,
|
||||||
|
description: None,
|
||||||
|
enum_: vec!["a".to_string(), "b".to_string()],
|
||||||
|
enum_names: None,
|
||||||
|
});
|
||||||
|
let json = serde_json::to_value(&schema)?;
|
||||||
|
assert!(!json.as_object().unwrap().contains_key("enumNames"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_enum_schema_titled_multi_select_serialization() -> anyhow::Result<()> {
|
fn test_enum_schema_titled_multi_select_serialization() -> anyhow::Result<()> {
|
||||||
let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
|
let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
|
||||||
|
|
|
||||||
|
|
@ -3642,6 +3642,7 @@
|
||||||
"$ref": "#/definitions/StringTypeConst"
|
"$ref": "#/definitions/StringTypeConst"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"enum"
|
"enum"
|
||||||
|
|
|
||||||
|
|
@ -3642,6 +3642,7 @@
|
||||||
"$ref": "#/definitions/StringTypeConst"
|
"$ref": "#/definitions/StringTypeConst"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"enum"
|
"enum"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue