Skip to main content

vtcode_utility_tool_specs/
json_schema.rs

1use 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}