1use serde_json::Value;
11
12pub fn validate_json(value: &Value, schema: &Value) -> Result<(), Vec<String>> {
17 let mut errors = Vec::new();
18 validate_inner(value, schema, "", &mut errors);
19 if errors.is_empty() {
20 Ok(())
21 } else {
22 Err(errors)
23 }
24}
25
26fn validate_inner(value: &Value, schema: &Value, path: &str, errors: &mut Vec<String>) {
27 if schema.is_boolean() || schema.as_object().is_some_and(|o| o.is_empty()) {
29 return;
30 }
31
32 let schema_obj = match schema.as_object() {
33 Some(obj) => obj,
34 None => return,
35 };
36
37 if let Some(enum_values) = schema_obj.get("enum").and_then(|v| v.as_array()) {
39 if !enum_values.contains(value) {
40 let allowed: Vec<String> = enum_values.iter().map(|v| v.to_string()).collect();
41 errors.push(format!(
42 "{}: value {} is not one of [{}]",
43 display_path(path),
44 value,
45 allowed.join(", ")
46 ));
47 }
48 }
49
50 if let Some(expected_type) = schema_obj.get("type").and_then(|v| v.as_str()) {
52 if !type_matches(value, expected_type) {
53 errors.push(format!(
54 "{}: expected type \"{}\", got {}",
55 display_path(path),
56 expected_type,
57 json_type_name(value)
58 ));
59 return; }
61 }
62
63 if let Some(obj) = value.as_object() {
65 if let Some(required) = schema_obj.get("required").and_then(|v| v.as_array()) {
67 for req in required {
68 if let Some(field_name) = req.as_str() {
69 if !obj.contains_key(field_name) {
70 errors.push(format!(
71 "{}: missing required field \"{}\"",
72 display_path(path),
73 field_name
74 ));
75 }
76 }
77 }
78 }
79
80 if let Some(properties) = schema_obj.get("properties").and_then(|v| v.as_object()) {
82 for (prop_name, prop_schema) in properties {
83 if let Some(prop_value) = obj.get(prop_name) {
84 let child_path = if path.is_empty() {
85 prop_name.clone()
86 } else {
87 format!("{}.{}", path, prop_name)
88 };
89 validate_inner(prop_value, prop_schema, &child_path, errors);
90 }
91 }
92 }
93 }
94
95 if let Some(arr) = value.as_array() {
97 if let Some(items_schema) = schema_obj.get("items") {
98 for (i, item) in arr.iter().enumerate() {
99 let child_path = format!("{}[{}]", path, i);
100 validate_inner(item, items_schema, &child_path, errors);
101 }
102 }
103 }
104}
105
106fn type_matches(value: &Value, expected: &str) -> bool {
107 match expected {
108 "object" => value.is_object(),
109 "string" => value.is_string(),
110 "number" => value.is_number(),
111 "integer" => value.is_i64() || value.is_u64(),
112 "boolean" => value.is_boolean(),
113 "array" => value.is_array(),
114 "null" => value.is_null(),
115 _ => true, }
117}
118
119fn json_type_name(value: &Value) -> &'static str {
120 match value {
121 Value::Null => "null",
122 Value::Bool(_) => "boolean",
123 Value::Number(n) => {
124 if n.is_i64() || n.is_u64() {
125 "integer"
126 } else {
127 "number"
128 }
129 }
130 Value::String(_) => "string",
131 Value::Array(_) => "array",
132 Value::Object(_) => "object",
133 }
134}
135
136fn display_path(path: &str) -> &str {
137 if path.is_empty() {
138 "$"
139 } else {
140 path
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use serde_json::json;
148
149 #[test]
150 fn empty_schema_accepts_anything() {
151 assert!(validate_json(&json!(42), &json!({})).is_ok());
152 assert!(validate_json(&json!("hello"), &json!({})).is_ok());
153 assert!(validate_json(&json!(null), &json!({})).is_ok());
154 }
155
156 #[test]
157 fn type_string_valid() {
158 let schema = json!({"type": "string"});
159 assert!(validate_json(&json!("hello"), &schema).is_ok());
160 }
161
162 #[test]
163 fn type_string_invalid() {
164 let schema = json!({"type": "string"});
165 let result = validate_json(&json!(42), &schema);
166 assert!(result.is_err());
167 let errors = result.unwrap_err();
168 assert_eq!(errors.len(), 1);
169 assert!(errors[0].contains("expected type \"string\""));
170 }
171
172 #[test]
173 fn type_object_valid() {
174 let schema = json!({"type": "object"});
175 assert!(validate_json(&json!({"a": 1}), &schema).is_ok());
176 }
177
178 #[test]
179 fn type_integer_valid() {
180 let schema = json!({"type": "integer"});
181 assert!(validate_json(&json!(42), &schema).is_ok());
182 }
183
184 #[test]
185 fn type_integer_rejects_float() {
186 let schema = json!({"type": "integer"});
187 let result = validate_json(&json!(2.5), &schema);
188 assert!(result.is_err());
189 }
190
191 #[test]
192 fn type_number_accepts_float_and_int() {
193 let schema = json!({"type": "number"});
194 assert!(validate_json(&json!(42), &schema).is_ok());
195 assert!(validate_json(&json!(2.5), &schema).is_ok());
196 }
197
198 #[test]
199 fn type_boolean_valid() {
200 let schema = json!({"type": "boolean"});
201 assert!(validate_json(&json!(true), &schema).is_ok());
202 }
203
204 #[test]
205 fn type_array_valid() {
206 let schema = json!({"type": "array"});
207 assert!(validate_json(&json!([1, 2, 3]), &schema).is_ok());
208 }
209
210 #[test]
211 fn type_null_valid() {
212 let schema = json!({"type": "null"});
213 assert!(validate_json(&json!(null), &schema).is_ok());
214 }
215
216 #[test]
217 fn required_fields_present() {
218 let schema = json!({
219 "type": "object",
220 "required": ["name", "age"],
221 "properties": {
222 "name": {"type": "string"},
223 "age": {"type": "integer"}
224 }
225 });
226 assert!(validate_json(&json!({"name": "Alice", "age": 30}), &schema).is_ok());
227 }
228
229 #[test]
230 fn required_field_missing() {
231 let schema = json!({
232 "type": "object",
233 "required": ["name", "age"],
234 "properties": {
235 "name": {"type": "string"},
236 "age": {"type": "integer"}
237 }
238 });
239 let result = validate_json(&json!({"name": "Alice"}), &schema);
240 assert!(result.is_err());
241 let errors = result.unwrap_err();
242 assert_eq!(errors.len(), 1);
243 assert!(errors[0].contains("missing required field \"age\""));
244 }
245
246 #[test]
247 fn nested_object_validation() {
248 let schema = json!({
249 "type": "object",
250 "properties": {
251 "address": {
252 "type": "object",
253 "required": ["city"],
254 "properties": {
255 "city": {"type": "string"},
256 "zip": {"type": "string"}
257 }
258 }
259 }
260 });
261 assert!(validate_json(&json!({"address": {"city": "NYC"}}), &schema).is_ok());
263 let result = validate_json(&json!({"address": {"city": 123}}), &schema);
265 assert!(result.is_err());
266 assert!(result.unwrap_err()[0].contains("address.city"));
267 }
268
269 #[test]
270 fn array_items_validation() {
271 let schema = json!({
272 "type": "array",
273 "items": {"type": "string"}
274 });
275 assert!(validate_json(&json!(["a", "b", "c"]), &schema).is_ok());
276
277 let result = validate_json(&json!(["a", 42, "c"]), &schema);
278 assert!(result.is_err());
279 let errors = result.unwrap_err();
280 assert_eq!(errors.len(), 1);
281 assert!(errors[0].contains("[1]"));
282 }
283
284 #[test]
285 fn enum_validation_valid() {
286 let schema = json!({
287 "type": "string",
288 "enum": ["red", "green", "blue"]
289 });
290 assert!(validate_json(&json!("red"), &schema).is_ok());
291 }
292
293 #[test]
294 fn enum_validation_invalid() {
295 let schema = json!({
296 "type": "string",
297 "enum": ["red", "green", "blue"]
298 });
299 let result = validate_json(&json!("yellow"), &schema);
300 assert!(result.is_err());
301 assert!(result.unwrap_err()[0].contains("not one of"));
302 }
303
304 #[test]
305 fn multiple_errors_reported() {
306 let schema = json!({
307 "type": "object",
308 "required": ["a", "b"],
309 "properties": {
310 "a": {"type": "string"},
311 "b": {"type": "integer"}
312 }
313 });
314 let result = validate_json(&json!({}), &schema);
316 assert!(result.is_err());
317 assert_eq!(result.unwrap_err().len(), 2);
318 }
319
320 #[test]
321 fn extra_properties_allowed() {
322 let schema = json!({
323 "type": "object",
324 "properties": {
325 "name": {"type": "string"}
326 }
327 });
328 assert!(validate_json(&json!({"name": "Alice", "extra": true}), &schema).is_ok());
330 }
331
332 #[test]
333 fn wrong_type_at_root_stops_early() {
334 let schema = json!({
335 "type": "object",
336 "required": ["name"],
337 "properties": {"name": {"type": "string"}}
338 });
339 let result = validate_json(&json!("not an object"), &schema);
340 assert!(result.is_err());
341 assert_eq!(result.unwrap_err().len(), 1);
343 }
344}