1pub mod error_mapper;
4
5use jsonschema::Validator;
6use serde_json::Value;
7use std::sync::Arc;
8
9use self::error_mapper::{ErrorCondition, ErrorMapper};
10
11#[derive(Clone)]
13pub struct SchemaValidator {
14 compiled: Arc<Validator>,
15 schema: Value,
16}
17
18impl std::fmt::Debug for SchemaValidator {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 f.debug_struct("SchemaValidator")
21 .field("schema", &self.schema)
22 .finish_non_exhaustive()
23 }
24}
25
26impl SchemaValidator {
27 pub fn new(schema: Value) -> Result<Self, String> {
32 let compiled = jsonschema::options()
33 .with_draft(jsonschema::Draft::Draft202012)
34 .should_validate_formats(true)
35 .with_pattern_options(jsonschema::PatternOptions::regex())
36 .build(&schema)
37 .map_err(|e| {
38 anyhow::anyhow!("Invalid JSON Schema")
39 .context(format!("Schema compilation failed: {e}"))
40 .to_string()
41 })?;
42
43 Ok(Self {
44 compiled: Arc::new(compiled),
45 schema,
46 })
47 }
48
49 #[must_use]
51 pub const fn schema(&self) -> &Value {
52 &self.schema
53 }
54
55 fn preprocess_binary_fields(&self, data: &Value) -> Value {
65 self.preprocess_value_with_schema(data, &self.schema)
66 }
67
68 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
69 fn preprocess_value_with_schema(&self, data: &Value, schema: &Value) -> Value {
70 if let Some(schema_obj) = schema.as_object() {
71 let is_string_type = schema_obj.get("type").and_then(|t| t.as_str()) == Some("string");
72 let is_binary_format = schema_obj.get("format").and_then(|f| f.as_str()) == Some("binary");
73
74 #[allow(clippy::collapsible_if)]
75 if is_string_type && is_binary_format {
76 if let Some(data_obj) = data.as_object() {
77 if data_obj.contains_key("filename")
78 && data_obj.contains_key("content")
79 && data_obj.contains_key("size")
80 && data_obj.contains_key("content_type")
81 {
82 return data_obj.get("content").unwrap_or(&Value::Null).clone();
83 }
84 }
85 return data.clone();
86 }
87
88 #[allow(clippy::collapsible_if)]
89 if schema_obj.get("type").and_then(|t| t.as_str()) == Some("array") {
90 if let Some(items_schema) = schema_obj.get("items") {
91 if let Some(data_array) = data.as_array() {
92 let processed_array: Vec<Value> = data_array
93 .iter()
94 .map(|item| self.preprocess_value_with_schema(item, items_schema))
95 .collect();
96 return Value::Array(processed_array);
97 }
98 }
99 }
100
101 #[allow(clippy::collapsible_if)]
102 if schema_obj.get("type").and_then(|t| t.as_str()) == Some("object") {
103 if let Some(properties) = schema_obj.get("properties").and_then(|p| p.as_object()) {
104 if let Some(data_obj) = data.as_object() {
105 let mut processed_obj = serde_json::Map::new();
106 for (key, value) in data_obj {
107 if let Some(prop_schema) = properties.get(key) {
108 processed_obj
109 .insert(key.clone(), self.preprocess_value_with_schema(value, prop_schema));
110 } else {
111 processed_obj.insert(key.clone(), value.clone());
112 }
113 }
114 return Value::Object(processed_obj);
115 }
116 }
117 }
118 }
119
120 data.clone()
121 }
122
123 #[allow(clippy::option_if_let_else, clippy::uninlined_format_args, clippy::too_many_lines)]
131 pub fn validate(&self, data: &Value) -> Result<(), ValidationError> {
132 let processed_data = self.preprocess_binary_fields(data);
133
134 let validation_errors: Vec<_> = self.compiled.iter_errors(&processed_data).collect();
135
136 if validation_errors.is_empty() {
137 return Ok(());
138 }
139
140 let errors: Vec<ValidationErrorDetail> = validation_errors
141 .into_iter()
142 .map(|err| {
143 let instance_path = err.instance_path().to_string();
144 let schema_path_str = err.schema_path().as_str();
145 let error_msg = err.to_string();
146
147 let param_name = if schema_path_str.ends_with("/required") {
148 let field_name = if let Some(start) = error_msg.find('"') {
149 if let Some(end) = error_msg[start + 1..].find('"') {
150 error_msg[start + 1..start + 1 + end].to_string()
151 } else {
152 String::new()
153 }
154 } else {
155 String::new()
156 };
157
158 if instance_path.starts_with('/') && instance_path.len() > 1 {
159 let base_path = &instance_path[1..];
160 if field_name.is_empty() {
161 base_path.to_string()
162 } else {
163 format!("{base_path}/{field_name}")
164 }
165 } else if field_name.is_empty() {
166 "body".to_string()
167 } else {
168 field_name
169 }
170 } else if schema_path_str.contains("/additionalProperties") {
171 if let Some(start) = error_msg.find('(') {
172 if let Some(quote_start) = error_msg[start..].find('\'') {
173 let abs_start = start + quote_start + 1;
174 error_msg[abs_start..].find('\'').map_or_else(
175 || instance_path[1..].to_string(),
176 |quote_end| {
177 let property_name = error_msg[abs_start..abs_start + quote_end].to_string();
178 if instance_path.starts_with('/') && instance_path.len() > 1 {
179 format!("{}/{property_name}", &instance_path[1..])
180 } else {
181 property_name
182 }
183 },
184 )
185 } else {
186 instance_path[1..].to_string()
187 }
188 } else if instance_path.starts_with('/') && instance_path.len() > 1 {
189 instance_path[1..].to_string()
190 } else {
191 "body".to_string()
192 }
193 } else if instance_path.starts_with('/') && instance_path.len() > 1 {
194 instance_path[1..].to_string()
195 } else if instance_path.is_empty() {
196 "body".to_string()
197 } else {
198 instance_path
199 };
200
201 let loc_parts: Vec<String> = if param_name.contains('/') {
202 let mut parts = vec!["body".to_string()];
203 parts.extend(param_name.split('/').map(ToString::to_string));
204 parts
205 } else if param_name == "body" {
206 vec!["body".to_string()]
207 } else {
208 vec!["body".to_string(), param_name.clone()]
209 };
210
211 let input_value = if schema_path_str == "/required" {
212 data.clone()
213 } else {
214 err.instance().clone().into_owned()
215 };
216
217 let schema_prop_path = if param_name.contains('/') {
218 format!("/properties/{}", param_name.replace('/', "/properties/"))
219 } else {
220 format!("/properties/{param_name}")
221 };
222
223 let mut error_condition = ErrorCondition::from_schema_error(schema_path_str, &error_msg);
224
225 error_condition = match error_condition {
226 ErrorCondition::TypeMismatch { .. } => {
227 let expected_type = self
228 .schema
229 .pointer(&format!("{schema_prop_path}/type"))
230 .and_then(|v| v.as_str())
231 .unwrap_or("unknown")
232 .to_string();
233 ErrorCondition::TypeMismatch { expected_type }
234 }
235 ErrorCondition::AdditionalProperties { .. } => {
236 #[allow(clippy::redundant_clone)]
237 let unexpected_field = if param_name.contains('/') {
238 param_name.split('/').next_back().unwrap_or(¶m_name).to_string()
239 } else {
240 param_name.clone()
241 };
242 ErrorCondition::AdditionalProperties {
243 field: unexpected_field,
244 }
245 }
246 other => other,
247 };
248
249 let (error_type, msg, ctx) =
250 ErrorMapper::map_error(&error_condition, &self.schema, &schema_prop_path, &error_msg);
251
252 ValidationErrorDetail {
253 error_type,
254 loc: loc_parts,
255 msg,
256 input: input_value,
257 ctx,
258 }
259 })
260 .collect();
261
262 Err(ValidationError { errors })
263 }
264
265 pub fn validate_json(&self, json_bytes: &[u8]) -> Result<Value, ValidationError> {
270 let value: Value = serde_json::from_slice(json_bytes).map_err(|e| ValidationError {
271 errors: vec![ValidationErrorDetail {
272 error_type: "json_parse_error".to_string(),
273 loc: vec!["body".to_string()],
274 msg: format!("Invalid JSON: {e}"),
275 input: Value::Null,
276 ctx: None,
277 }],
278 })?;
279
280 self.validate(&value)?;
281
282 Ok(value)
283 }
284}
285
286#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
288pub struct ValidationError {
289 pub errors: Vec<ValidationErrorDetail>,
290}
291
292#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
294pub struct ValidationErrorDetail {
295 #[serde(rename = "type")]
296 pub error_type: String,
297 pub loc: Vec<String>,
298 pub msg: String,
299 pub input: Value,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub ctx: Option<Value>,
302}
303
304impl std::fmt::Display for ValidationError {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 write!(f, "Validation failed: {} errors", self.errors.len())
307 }
308}
309
310impl std::error::Error for ValidationError {}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use serde_json::json;
316
317 #[test]
318 fn test_validator_creation() {
319 let schema = json!({
320 "type": "object",
321 "properties": {
322 "name": {"type": "string"},
323 "age": {"type": "integer"}
324 },
325 "required": ["name"]
326 });
327
328 let validator = SchemaValidator::new(schema).unwrap();
329 assert!(validator.compiled.is_valid(&json!({"name": "Alice", "age": 30})));
330 }
331
332 #[test]
333 fn test_validation_success() {
334 let schema = json!({
335 "type": "object",
336 "properties": {
337 "email": {"type": "string", "format": "email"}
338 }
339 });
340
341 let validator = SchemaValidator::new(schema).unwrap();
342 let data = json!({"email": "test@example.com"});
343
344 assert!(validator.validate(&data).is_ok());
345 }
346
347 #[test]
348 fn test_validation_failure() {
349 let schema = json!({
350 "type": "object",
351 "properties": {
352 "age": {"type": "integer", "minimum": 0}
353 },
354 "required": ["age"]
355 });
356
357 let validator = SchemaValidator::new(schema).unwrap();
358 let data = json!({"age": -5});
359
360 assert!(validator.validate(&data).is_err());
361 }
362
363 #[test]
364 fn test_validation_error_serialization() {
365 let schema = json!({
366 "type": "object",
367 "properties": {
368 "name": {
369 "type": "string",
370 "maxLength": 10
371 }
372 },
373 "required": ["name"]
374 });
375
376 let validator = SchemaValidator::new(schema).unwrap();
377 let data = json!({"name": "this_is_way_too_long"});
378
379 let result = validator.validate(&data);
380 assert!(result.is_err());
381
382 let err = result.unwrap_err();
383 assert_eq!(err.errors.len(), 1);
384
385 let error_detail = &err.errors[0];
386 assert_eq!(error_detail.error_type, "string_too_long");
387 assert_eq!(error_detail.loc, vec!["body", "name"]);
388 assert_eq!(error_detail.msg, "String should have at most 10 characters");
389 assert_eq!(error_detail.input, Value::String("this_is_way_too_long".to_string()));
390 assert_eq!(error_detail.ctx, Some(json!({"max_length": 10})));
391
392 let json_output = serde_json::to_value(&err.errors).unwrap();
393 println!(
394 "Serialized JSON: {}",
395 serde_json::to_string_pretty(&json_output).unwrap()
396 );
397
398 let serialized_error = &json_output[0];
399 assert!(serialized_error.get("type").is_some());
400 assert!(serialized_error.get("loc").is_some());
401 assert!(serialized_error.get("msg").is_some());
402 assert!(
403 serialized_error.get("input").is_some(),
404 "Missing 'input' field in serialized JSON!"
405 );
406 assert!(
407 serialized_error.get("ctx").is_some(),
408 "Missing 'ctx' field in serialized JSON!"
409 );
410
411 assert_eq!(
412 serialized_error["input"],
413 Value::String("this_is_way_too_long".to_string())
414 );
415 assert_eq!(serialized_error["ctx"], json!({"max_length": 10}));
416 }
417
418 #[test]
419 fn test_exclusive_minimum() {
420 let schema = json!({
421 "$schema": "https://json-schema.org/draft/2020-12/schema",
422 "type": "object",
423 "required": ["id", "name", "price"],
424 "properties": {
425 "id": {
426 "type": "integer"
427 },
428 "name": {
429 "type": "string",
430 "minLength": 3
431 },
432 "price": {
433 "type": "number",
434 "exclusiveMinimum": 0
435 }
436 }
437 });
438
439 let validator = SchemaValidator::new(schema).unwrap();
440
441 let data = json!({
442 "id": 1,
443 "name": "X",
444 "price": -10
445 });
446
447 let result = validator.validate(&data);
448 eprintln!("Validation result: {result:?}");
449
450 assert!(result.is_err(), "Should have validation errors");
451 let err = result.unwrap_err();
452 eprintln!("Errors: {:?}", err.errors);
453 assert_eq!(err.errors.len(), 2, "Should have 2 errors");
454 }
455}