vtcode_utility_tool_specs/
json_schema.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "lowercase")]
7pub enum JsonSchema {
8 Object {
9 #[serde(default)]
10 properties: BTreeMap<String, JsonSchema>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 required: Option<Vec<String>>,
13 #[serde(
14 rename = "additionalProperties",
15 skip_serializing_if = "Option::is_none"
16 )]
17 additional_properties: Option<AdditionalProperties>,
18 #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
19 any_of: Option<Vec<Value>>,
20 },
21 String {
22 #[serde(skip_serializing_if = "Option::is_none")]
23 description: Option<String>,
24 },
25 Number {
26 #[serde(skip_serializing_if = "Option::is_none")]
27 description: Option<String>,
28 },
29 Boolean {
30 #[serde(skip_serializing_if = "Option::is_none")]
31 description: Option<String>,
32 },
33 Array {
34 items: Box<JsonSchema>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 description: Option<String>,
37 },
38 Null,
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum AdditionalProperties {
44 Boolean(bool),
45 Schema(Box<JsonSchema>),
46}
47
48impl From<bool> for AdditionalProperties {
49 fn from(value: bool) -> Self {
50 Self::Boolean(value)
51 }
52}
53
54#[must_use]
55pub fn parse_tool_input_schema(value: &Value) -> JsonSchema {
56 match value {
57 Value::Object(map) => match map.get("type").and_then(Value::as_str) {
58 Some("object") => {
59 let properties = map
60 .get("properties")
61 .and_then(Value::as_object)
62 .map(|props| {
63 props
64 .iter()
65 .map(|(key, value)| (key.clone(), parse_tool_input_schema(value)))
66 .collect()
67 })
68 .unwrap_or_default();
69 let required = map.get("required").and_then(Value::as_array).map(|items| {
70 items
71 .iter()
72 .filter_map(Value::as_str)
73 .map(ToOwned::to_owned)
74 .collect::<Vec<_>>()
75 });
76 let additional_properties =
77 map.get("additionalProperties").map(|value| match value {
78 Value::Bool(flag) => AdditionalProperties::Boolean(*flag),
79 Value::Object(_) => {
80 AdditionalProperties::Schema(Box::new(parse_tool_input_schema(value)))
81 }
82 _ => AdditionalProperties::Boolean(true),
83 });
84 let any_of = map.get("anyOf").and_then(Value::as_array).cloned();
85
86 JsonSchema::Object {
87 properties,
88 required,
89 additional_properties,
90 any_of,
91 }
92 }
93 Some("array") => JsonSchema::Array {
94 items: Box::new(
95 map.get("items")
96 .map(parse_tool_input_schema)
97 .unwrap_or(JsonSchema::Null),
98 ),
99 description: map
100 .get("description")
101 .and_then(Value::as_str)
102 .map(ToOwned::to_owned),
103 },
104 Some("boolean") => JsonSchema::Boolean {
105 description: map
106 .get("description")
107 .and_then(Value::as_str)
108 .map(ToOwned::to_owned),
109 },
110 Some("integer" | "number") => JsonSchema::Number {
111 description: map
112 .get("description")
113 .and_then(Value::as_str)
114 .map(ToOwned::to_owned),
115 },
116 Some("string") => JsonSchema::String {
117 description: map
118 .get("description")
119 .and_then(Value::as_str)
120 .map(ToOwned::to_owned),
121 },
122 _ => {
123 if map.contains_key("enum") {
124 JsonSchema::String {
125 description: map
126 .get("description")
127 .and_then(Value::as_str)
128 .map(ToOwned::to_owned),
129 }
130 } else {
131 JsonSchema::Null
132 }
133 }
134 },
135 _ => JsonSchema::Null,
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
142 use serde_json::{Value, json};
143
144 #[test]
145 fn parse_tool_input_schema_preserves_schema_field_names() {
146 let schema = parse_tool_input_schema(&json!({
147 "type": "object",
148 "properties": {
149 "input": {"type": "string"}
150 },
151 "additionalProperties": false,
152 "anyOf": [
153 {"required": ["input"]},
154 {"required": ["patch"]}
155 ]
156 }));
157
158 let serialized = serde_json::to_value(&schema).expect("serialize schema");
159 assert_eq!(serialized["additionalProperties"], Value::Bool(false));
160 assert!(serialized["anyOf"].is_array());
161 assert!(serialized.get("additional_properties").is_none());
162 assert!(serialized.get("any_of").is_none());
163 }
164
165 #[test]
166 fn parse_tool_input_schema_parses_object_additional_properties_schema() {
167 let schema = parse_tool_input_schema(&json!({
168 "type": "object",
169 "additionalProperties": {
170 "type": "string",
171 "description": "value"
172 }
173 }));
174
175 let JsonSchema::Object {
176 additional_properties,
177 ..
178 } = schema
179 else {
180 panic!("expected object schema");
181 };
182
183 let Some(AdditionalProperties::Schema(nested)) = additional_properties else {
184 panic!("expected nested additional properties schema");
185 };
186
187 match *nested {
188 JsonSchema::String { description } => {
189 assert_eq!(description.as_deref(), Some("value"));
190 }
191 other => panic!("expected string schema, got {other:?}"),
192 }
193 }
194}