zag_agent/
json_validation.rs1use log::debug;
4
5pub fn strip_markdown_fences(text: &str) -> &str {
7 let trimmed = text.trim();
8 if let Some(rest) = trimmed.strip_prefix("```json") {
9 rest.strip_suffix("```").unwrap_or(rest).trim()
10 } else if let Some(rest) = trimmed.strip_prefix("```") {
11 rest.strip_suffix("```").unwrap_or(rest).trim()
12 } else {
13 trimmed
14 }
15}
16
17pub fn validate_json(text: &str) -> Result<serde_json::Value, String> {
21 let cleaned = strip_markdown_fences(text);
22 debug!("Validating JSON ({} bytes)", cleaned.len());
23 let result = serde_json::from_str(cleaned).map_err(|e| format!("Invalid JSON: {}", e));
24 if result.is_ok() {
25 debug!("JSON validation passed");
26 } else {
27 debug!("JSON validation failed");
28 }
29 result
30}
31
32pub fn validate_schema(schema: &serde_json::Value) -> Result<(), String> {
36 debug!("Validating JSON schema");
37 jsonschema::validator_for(schema)
38 .map(|_| {
39 debug!("JSON schema is valid");
40 })
41 .map_err(|e| format!("Invalid JSON schema: {}", e))
42}
43
44pub fn validate_json_schema(
48 text: &str,
49 schema: &serde_json::Value,
50) -> Result<serde_json::Value, Vec<String>> {
51 let cleaned = strip_markdown_fences(text);
52 debug!("Validating JSON ({} bytes) against schema", cleaned.len());
53 let value: serde_json::Value = serde_json::from_str(cleaned).map_err(|e| {
54 debug!(
55 "JSON parse failed on input ({} bytes): {:.200}",
56 cleaned.len(),
57 cleaned
58 );
59 vec![format!("Invalid JSON: {}", e)]
60 })?;
61
62 let validator = jsonschema::validator_for(schema)
63 .map_err(|e| vec![format!("Invalid JSON schema: {}", e)])?;
64
65 let errors: Vec<String> = validator
66 .iter_errors(&value)
67 .map(|e| {
68 let path = e.instance_path.to_string();
69 if path.is_empty() {
70 e.to_string()
71 } else {
72 format!("{} at {}", e, path)
73 }
74 })
75 .collect();
76
77 if errors.is_empty() {
78 debug!("JSON schema validation passed");
79 Ok(value)
80 } else {
81 debug!("JSON schema validation failed with {} errors", errors.len());
82 Err(errors)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_validate_json_valid() {
92 let result = validate_json(r#"{"key": "value"}"#);
93 assert!(result.is_ok());
94 assert_eq!(result.unwrap()["key"], "value");
95 }
96
97 #[test]
98 fn test_validate_json_invalid() {
99 let result = validate_json("not json at all");
100 assert!(result.is_err());
101 assert!(result.unwrap_err().contains("Invalid JSON"));
102 }
103
104 #[test]
105 fn test_validate_json_with_markdown_fences() {
106 let result = validate_json("```json\n{\"key\": \"value\"}\n```");
107 assert!(result.is_ok());
108 assert_eq!(result.unwrap()["key"], "value");
109 }
110
111 #[test]
112 fn test_validate_json_with_generic_fences() {
113 let result = validate_json("```\n{\"key\": \"value\"}\n```");
114 assert!(result.is_ok());
115 }
116
117 #[test]
118 fn test_validate_json_array() {
119 let result = validate_json("[1, 2, 3]");
120 assert!(result.is_ok());
121 }
122
123 #[test]
124 fn test_validate_json_schema_valid() {
125 let schema: serde_json::Value = serde_json::json!({
126 "type": "object",
127 "properties": {
128 "name": {"type": "string"}
129 },
130 "required": ["name"]
131 });
132 let result = validate_json_schema(r#"{"name": "test"}"#, &schema);
133 assert!(result.is_ok());
134 }
135
136 #[test]
137 fn test_validate_json_schema_invalid_missing_required() {
138 let schema: serde_json::Value = serde_json::json!({
139 "type": "object",
140 "properties": {
141 "name": {"type": "string"}
142 },
143 "required": ["name"]
144 });
145 let result = validate_json_schema(r#"{"other": "value"}"#, &schema);
146 assert!(result.is_err());
147 let errors = result.unwrap_err();
148 assert!(!errors.is_empty());
149 }
150
151 #[test]
152 fn test_validate_json_schema_invalid_wrong_type() {
153 let schema: serde_json::Value = serde_json::json!({
154 "type": "object",
155 "properties": {
156 "count": {"type": "integer"}
157 }
158 });
159 let result = validate_json_schema(r#"{"count": "not a number"}"#, &schema);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn test_validate_json_schema_with_fences() {
165 let schema: serde_json::Value = serde_json::json!({
166 "type": "object",
167 "properties": {
168 "items": {"type": "array"}
169 }
170 });
171 let result = validate_json_schema("```json\n{\"items\": [1,2,3]}\n```", &schema);
172 assert!(result.is_ok());
173 }
174
175 #[test]
176 fn test_validate_json_schema_root_error_no_dangling_at() {
177 let schema: serde_json::Value = serde_json::json!({
178 "type": "object",
179 "required": ["languages"]
180 });
181 let result = validate_json_schema(r#"{"other": "value"}"#, &schema);
182 assert!(result.is_err());
183 let errors = result.unwrap_err();
184 assert_eq!(errors.len(), 1);
185 assert!(
187 !errors[0].ends_with(" at"),
188 "Error message has dangling 'at': {}",
189 errors[0]
190 );
191 assert!(
192 !errors[0].ends_with(" at "),
193 "Error message has dangling 'at ': {}",
194 errors[0]
195 );
196 }
197
198 #[test]
199 fn test_validate_json_schema_nested_error_includes_path() {
200 let schema: serde_json::Value = serde_json::json!({
201 "type": "object",
202 "properties": {
203 "user": {
204 "type": "object",
205 "properties": {
206 "age": {"type": "integer"}
207 }
208 }
209 }
210 });
211 let result = validate_json_schema(r#"{"user": {"age": "not a number"}}"#, &schema);
212 assert!(result.is_err());
213 let errors = result.unwrap_err();
214 assert!(!errors.is_empty());
215 assert!(
216 errors[0].contains(" at "),
217 "Nested error should include path: {}",
218 errors[0]
219 );
220 }
221
222 #[test]
223 fn test_validate_schema_accepts_valid_schema() {
224 let schema: serde_json::Value = serde_json::json!({
225 "type": "object",
226 "properties": {
227 "name": {"type": "string"}
228 }
229 });
230 assert!(validate_schema(&schema).is_ok());
231 }
232
233 #[test]
234 fn test_validate_schema_rejects_invalid_schema() {
235 let schema: serde_json::Value = serde_json::json!({
236 "type": "not-a-real-type"
237 });
238 let result = validate_schema(&schema);
239 assert!(result.is_err());
240 assert!(result.unwrap_err().contains("Invalid JSON schema"));
241 }
242
243 #[test]
244 fn test_strip_markdown_fences_no_fences() {
245 assert_eq!(
246 strip_markdown_fences(r#"{"key": "value"}"#),
247 r#"{"key": "value"}"#
248 );
249 }
250
251 #[test]
252 fn test_strip_markdown_fences_json_fences() {
253 assert_eq!(
254 strip_markdown_fences("```json\n{\"key\": \"value\"}\n```"),
255 "{\"key\": \"value\"}"
256 );
257 }
258
259 #[test]
260 fn test_validate_json_empty_string() {
261 let result = validate_json("");
262 assert!(result.is_err());
263 }
264
265 #[test]
266 fn test_validate_json_whitespace_only() {
267 let result = validate_json(" \n\t ");
268 assert!(result.is_err());
269 }
270
271 #[test]
272 fn test_validate_json_schema_additional_properties() {
273 let schema: serde_json::Value = serde_json::json!({
274 "type": "object",
275 "properties": {
276 "name": {"type": "string"}
277 },
278 "additionalProperties": false
279 });
280 let result = validate_json_schema(r#"{"name": "test", "extra": true}"#, &schema);
281 assert!(result.is_err());
282 }
283
284 #[test]
285 fn test_strip_markdown_fences_with_whitespace() {
286 assert_eq!(
287 strip_markdown_fences(" ```json\n{\"key\": \"value\"}\n``` "),
288 "{\"key\": \"value\"}"
289 );
290 }
291}