vtcode_utility_tool_specs/
json_schema.rs1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value, json};
3
4pub type JsonSchema = Value;
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
7#[serde(untagged)]
8pub enum AdditionalProperties {
9 Boolean(bool),
10 Schema(Box<JsonSchema>),
11}
12
13impl From<bool> for AdditionalProperties {
14 fn from(value: bool) -> Self {
15 Self::Boolean(value)
16 }
17}
18
19#[must_use]
20pub fn parse_tool_input_schema(value: &Value) -> JsonSchema {
21 let mut schema = value.clone();
22 sanitize_json_schema(&mut schema);
23 schema
24}
25
26fn sanitize_json_schema(value: &mut Value) {
27 match value {
28 Value::Bool(_) => {
29 *value = json!({ "type": "string" });
30 }
31 Value::Object(map) => sanitize_schema_object(map),
32 Value::Array(items) => {
33 for item in items {
34 sanitize_json_schema(item);
35 }
36 }
37 Value::Null | Value::Number(_) | Value::String(_) => {}
38 }
39}
40
41fn sanitize_schema_object(map: &mut Map<String, Value>) {
42 if let Some(properties) = map.get_mut("properties").and_then(Value::as_object_mut) {
43 for schema in properties.values_mut() {
44 sanitize_json_schema(schema);
45 }
46 }
47
48 if let Some(items) = map.get_mut("items") {
49 sanitize_json_schema(items);
50 }
51
52 if let Some(prefix_items) = map.get_mut("prefixItems") {
53 sanitize_json_schema(prefix_items);
54 }
55
56 if let Some(additional_properties) = map.get_mut("additionalProperties")
57 && !matches!(additional_properties, Value::Bool(_))
58 {
59 sanitize_json_schema(additional_properties);
60 }
61
62 if let Some(any_of) = map.get_mut("anyOf") {
63 sanitize_json_schema(any_of);
64 }
65
66 if let Some(const_value) = map.remove("const") {
67 map.insert("enum".to_string(), Value::Array(vec![const_value]));
68 }
69
70 let mut schema_types = normalized_schema_types(map);
71 if schema_types.is_empty() && map.contains_key("anyOf") {
72 return;
73 }
74
75 if schema_types.is_empty() {
76 if map.contains_key("properties")
77 || map.contains_key("required")
78 || map.contains_key("additionalProperties")
79 {
80 schema_types.push("object");
81 } else if map.contains_key("items") || map.contains_key("prefixItems") {
82 schema_types.push("array");
83 } else if map.contains_key("enum") || map.contains_key("format") {
84 schema_types.push("string");
85 } else if map.contains_key("minimum")
86 || map.contains_key("maximum")
87 || map.contains_key("exclusiveMinimum")
88 || map.contains_key("exclusiveMaximum")
89 || map.contains_key("multipleOf")
90 {
91 schema_types.push("number");
92 } else {
93 schema_types.push("string");
94 }
95 }
96
97 write_schema_types(map, &schema_types);
98 ensure_default_children_for_schema_types(map, &schema_types);
99}
100
101fn normalized_schema_types(map: &Map<String, Value>) -> Vec<&'static str> {
102 let Some(schema_type) = map.get("type") else {
103 return Vec::new();
104 };
105
106 match schema_type {
107 Value::String(schema_type) => schema_type_from_str(schema_type).into_iter().collect(),
108 Value::Array(schema_types) => schema_types
109 .iter()
110 .filter_map(Value::as_str)
111 .filter_map(schema_type_from_str)
112 .collect(),
113 _ => Vec::new(),
114 }
115}
116
117fn write_schema_types(map: &mut Map<String, Value>, schema_types: &[&'static str]) {
118 match schema_types {
119 [] => {
120 map.remove("type");
121 }
122 [schema_type] => {
123 map.insert(
124 "type".to_string(),
125 Value::String((*schema_type).to_string()),
126 );
127 }
128 _ => {
129 map.insert(
130 "type".to_string(),
131 Value::Array(
132 schema_types
133 .iter()
134 .map(|schema_type| Value::String((*schema_type).to_string()))
135 .collect(),
136 ),
137 );
138 }
139 }
140}
141
142fn ensure_default_children_for_schema_types(map: &mut Map<String, Value>, schema_types: &[&str]) {
143 if schema_types.contains(&"object") && !map.contains_key("properties") {
144 map.insert("properties".to_string(), Value::Object(Map::new()));
145 }
146
147 if schema_types.contains(&"array") && !map.contains_key("items") {
148 map.insert("items".to_string(), json!({ "type": "string" }));
149 }
150}
151
152fn schema_type_from_str(schema_type: &str) -> Option<&'static str> {
153 match schema_type {
154 "string" => Some("string"),
155 "number" => Some("number"),
156 "integer" => Some("integer"),
157 "boolean" => Some("boolean"),
158 "object" => Some("object"),
159 "array" => Some("array"),
160 "null" => Some("null"),
161 _ => None,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::parse_tool_input_schema;
168 use serde_json::{Value, json};
169
170 #[test]
171 fn parse_tool_input_schema_preserves_schema_field_names() {
172 let schema = parse_tool_input_schema(&json!({
173 "type": "object",
174 "properties": {
175 "input": {"type": "string"}
176 },
177 "additionalProperties": false,
178 "anyOf": [
179 {"required": ["input"]},
180 {"required": ["patch"]}
181 ]
182 }));
183
184 let serialized = serde_json::to_value(&schema).expect("serialize schema");
185 assert_eq!(serialized["additionalProperties"], Value::Bool(false));
186 assert!(serialized["anyOf"].is_array());
187 assert!(serialized.get("additional_properties").is_none());
188 assert!(serialized.get("any_of").is_none());
189 }
190
191 #[test]
192 fn parse_tool_input_schema_parses_object_additional_properties_schema() {
193 let schema = parse_tool_input_schema(&json!({
194 "type": "object",
195 "additionalProperties": {
196 "type": "string",
197 "description": "value"
198 }
199 }));
200
201 assert_eq!(schema["type"], "object");
202 assert_eq!(schema["additionalProperties"]["type"], "string");
203 assert_eq!(schema["additionalProperties"]["description"], "value");
204 }
205
206 #[test]
207 fn parse_tool_input_schema_preserves_nested_any_of_and_nullable_type_unions() {
208 let schema = parse_tool_input_schema(&json!({
209 "type": "object",
210 "properties": {
211 "open": {
212 "anyOf": [
213 {
214 "type": "array",
215 "items": {
216 "type": "object",
217 "properties": {
218 "ref_id": {"type": "string"},
219 "lineno": {"type": ["integer", "null"]}
220 },
221 "required": ["ref_id"],
222 "additionalProperties": false
223 }
224 },
225 {"type": "null"}
226 ]
227 },
228 "message": {"type": ["string", "null"]}
229 },
230 "additionalProperties": false
231 }));
232
233 let variants = schema["properties"]["open"]["anyOf"]
234 .as_array()
235 .expect("open anyOf");
236 assert_eq!(variants.len(), 2);
237 assert_eq!(variants[0]["type"], "array");
238 assert_eq!(variants[0]["items"]["type"], "object");
239 assert_eq!(
240 variants[0]["items"]["properties"]["lineno"]["type"],
241 json!(["integer", "null"])
242 );
243 assert_eq!(
244 schema["properties"]["message"]["type"],
245 json!(["string", "null"])
246 );
247 }
248
249 #[test]
250 fn parse_tool_input_schema_preserves_integer_and_string_enums() {
251 let schema = parse_tool_input_schema(&json!({
252 "type": "object",
253 "properties": {
254 "page": {"type": "integer"},
255 "response_length": {
256 "type": "string",
257 "enum": ["short", "medium", "long"]
258 },
259 "kind": {
260 "type": "const",
261 "const": "tagged"
262 }
263 }
264 }));
265
266 assert_eq!(schema["properties"]["page"]["type"], "integer");
267 assert_eq!(
268 schema["properties"]["response_length"]["enum"],
269 json!(["short", "medium", "long"])
270 );
271 assert_eq!(schema["properties"]["kind"]["type"], "string");
272 assert_eq!(schema["properties"]["kind"]["enum"], json!(["tagged"]));
273 }
274}