use serde_json::Value;
pub fn normalize_tool_schema(schema: &Value) -> Option<Value> {
let obj = schema.as_object()?;
let mut result = obj.clone();
if result.get("type").is_some_and(|t| t.is_array()) {
result.remove("type");
return Some(Value::Object(result));
}
if result
.get("additionalProperties")
.is_some_and(|ap| ap == &Value::Bool(true))
{
result.remove("additionalProperties");
}
let is_object = result
.get("type")
.and_then(|t| t.as_str())
.map(|s| s == "object")
.unwrap_or(false);
if is_object {
if !result.contains_key("properties") {
result.insert("properties".to_string(), Value::Object(Default::default()));
}
if let Some(Value::Object(props)) = result.get("properties").cloned() {
let mut normalized_props = serde_json::Map::new();
for (key, prop_schema) in props {
if let Some(normalized) = normalize_tool_schema(&prop_schema) {
normalized_props.insert(key, normalized);
} else {
normalized_props.insert(key, prop_schema);
}
}
result.insert("properties".to_string(), Value::Object(normalized_props));
}
}
let is_array = result
.get("type")
.and_then(|t| t.as_str())
.map(|s| s == "array")
.unwrap_or(false);
if is_array
&& let Some(items) = result.get("items").cloned()
&& let Some(normalized_items) = normalize_tool_schema(&items)
{
result.insert("items".to_string(), normalized_items);
}
Some(Value::Object(result))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn normalizes_schema_missing_properties() {
let schema = json!({"type": "object", "additionalProperties": true});
let result = normalize_tool_schema(&schema).unwrap();
assert_eq!(result, json!({"type": "object", "properties": {}}));
}
#[test]
fn normalizes_union_type() {
let schema = json!({
"type": "object",
"properties": {
"value": {
"type": ["string", "boolean", "number"]
}
}
});
let result = normalize_tool_schema(&schema).unwrap();
let value_prop = &result["properties"]["value"];
assert!(value_prop.get("type").is_none());
}
#[test]
fn preserves_valid_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result = normalize_tool_schema(&schema).unwrap();
assert_eq!(result, schema);
}
#[test]
fn normalizes_nested_objects() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"additionalProperties": true
}
}
});
let result = normalize_tool_schema(&schema).unwrap();
let address = &result["properties"]["address"];
assert_eq!(*address, json!({"type": "object", "properties": {}}));
}
#[test]
fn returns_none_for_non_object() {
assert!(normalize_tool_schema(&json!("string")).is_none());
assert!(normalize_tool_schema(&json!(42)).is_none());
assert!(normalize_tool_schema(&json!(null)).is_none());
assert!(normalize_tool_schema(&json!(true)).is_none());
}
#[test]
fn preserves_additional_properties_false() {
let schema = json!({
"type": "object",
"properties": {"x": {"type": "string"}},
"additionalProperties": false
});
let result = normalize_tool_schema(&schema).unwrap();
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn normalizes_array_items() {
let schema = json!({
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
});
let result = normalize_tool_schema(&schema).unwrap();
let items = &result["properties"]["tags"]["items"];
assert_eq!(*items, json!({"type": "object", "properties": {}}));
}
}