1use serde_json::Value;
26use std::collections::HashMap;
27use turul_mcp_protocol::ToolSchema;
28use turul_mcp_protocol::schema::JsonSchema;
29
30pub fn convert_value_to_json_schema(value: &Value) -> JsonSchema {
38 convert_value_to_json_schema_with_defs(value, &HashMap::new())
39}
40
41pub fn convert_value_to_json_schema_with_defs(
60 value: &Value,
61 definitions: &HashMap<String, Value>,
62) -> JsonSchema {
63 if let Some(b) = value.as_bool() {
65 return JsonSchema::Object {
68 description: None,
69 properties: None,
70 required: None,
71 additional_properties: Some(b),
72 };
73 }
74
75 let obj = match value.as_object() {
77 Some(o) => o,
78 None => {
79 return JsonSchema::Object {
81 description: None,
82 properties: None,
83 required: None,
84 additional_properties: None,
85 };
86 }
87 };
88
89 if let Some(ref_path) = obj.get("$ref").and_then(|v| v.as_str()) {
91 let def_name = ref_path
93 .strip_prefix("#/definitions/")
94 .or_else(|| ref_path.strip_prefix("#/$defs/"));
95
96 if let Some(name) = def_name
97 && let Some(def_schema) = definitions.get(name)
98 {
99 return convert_value_to_json_schema_with_defs(def_schema, definitions);
101 }
102 return JsonSchema::Object {
104 description: obj
105 .get("description")
106 .and_then(|v| v.as_str())
107 .map(String::from),
108 properties: None,
109 required: None,
110 additional_properties: None,
111 };
112 }
113
114 if let Some(any_of) = obj.get("anyOf").and_then(|v| v.as_array()) {
116 for schema in any_of {
118 if let Some(obj) = schema.as_object() {
120 if let Some(t) = obj.get("type")
121 && t.as_str() == Some("null")
122 {
123 continue; }
125 return convert_value_to_json_schema_with_defs(schema, definitions);
127 }
128 }
129 return JsonSchema::Object {
131 description: obj
132 .get("description")
133 .and_then(|v| v.as_str())
134 .map(String::from),
135 properties: None,
136 required: None,
137 additional_properties: None,
138 };
139 }
140
141 let schema_type = obj
143 .get("type")
144 .and_then(|v| {
145 if let Some(s) = v.as_str() {
146 Some(s.to_string())
148 } else if let Some(arr) = v.as_array() {
149 for type_val in arr {
152 if let Some(t) = type_val.as_str()
153 && t != "null"
154 {
155 return Some(t.to_string());
156 }
157 }
158 None
159 } else {
160 None
161 }
162 })
163 .or_else(|| {
164 if obj.contains_key("properties") {
166 Some("object".to_string())
167 } else {
168 None
169 }
170 });
171
172 let schema_type = schema_type.as_deref();
173 match schema_type {
177 Some("string") => JsonSchema::String {
178 description: obj
179 .get("description")
180 .and_then(|v| v.as_str())
181 .map(String::from),
182 pattern: obj
183 .get("pattern")
184 .and_then(|v| v.as_str())
185 .map(String::from),
186 min_length: obj.get("minLength").and_then(|v| v.as_u64()),
187 max_length: obj.get("maxLength").and_then(|v| v.as_u64()),
188 enum_values: obj.get("enum").and_then(|v| {
189 v.as_array().and_then(|arr| {
190 arr.iter()
191 .map(|v| v.as_str().map(String::from))
192 .collect::<Option<Vec<_>>>()
193 })
194 }),
195 },
196
197 Some("number") => JsonSchema::Number {
198 description: obj
199 .get("description")
200 .and_then(|v| v.as_str())
201 .map(String::from),
202 minimum: obj.get("minimum").and_then(|v| v.as_f64()),
203 maximum: obj.get("maximum").and_then(|v| v.as_f64()),
204 },
205
206 Some("integer") => JsonSchema::Integer {
207 description: obj
208 .get("description")
209 .and_then(|v| v.as_str())
210 .map(String::from),
211 minimum: obj.get("minimum").and_then(|v| v.as_i64()),
212 maximum: obj.get("maximum").and_then(|v| v.as_i64()),
213 },
214
215 Some("boolean") => JsonSchema::Boolean {
216 description: obj
217 .get("description")
218 .and_then(|v| v.as_str())
219 .map(String::from),
220 },
221
222 Some("array") => {
223 let items = obj
225 .get("items")
226 .map(|v| Box::new(convert_value_to_json_schema_with_defs(v, definitions)));
227
228 JsonSchema::Array {
229 description: obj
230 .get("description")
231 .and_then(|v| v.as_str())
232 .map(String::from),
233 items,
234 min_items: obj.get("minItems").and_then(|v| v.as_u64()),
235 max_items: obj.get("maxItems").and_then(|v| v.as_u64()),
236 }
237 }
238
239 Some("object") => {
240 let properties = obj
242 .get("properties")
243 .and_then(|v| v.as_object())
244 .map(|props| {
245 props
246 .iter()
247 .map(|(k, v)| {
248 (
249 k.clone(),
250 convert_value_to_json_schema_with_defs(v, definitions),
251 )
252 })
253 .collect::<HashMap<_, _>>()
254 });
255
256 let required = obj.get("required").and_then(|v| v.as_array()).map(|arr| {
258 arr.iter()
259 .filter_map(|v| v.as_str().map(String::from))
260 .collect()
261 });
262
263 JsonSchema::Object {
264 description: obj
265 .get("description")
266 .and_then(|v| v.as_str())
267 .map(String::from),
268 properties,
269 required,
270 additional_properties: obj.get("additionalProperties").and_then(|v| v.as_bool()),
271 }
272 }
273
274 _ => {
275 JsonSchema::Object {
278 description: obj
279 .get("description")
280 .and_then(|v| v.as_str())
281 .map(String::from),
282 properties: None,
283 required: None,
284 additional_properties: None,
285 }
286 }
287 }
288}
289
290pub trait ToolSchemaExt {
295 fn from_schemars(schema: schemars::Schema) -> Result<Self, String>
335 where
336 Self: Sized;
337}
338
339impl ToolSchemaExt for ToolSchema {
340 fn from_schemars(schema: schemars::Schema) -> Result<Self, String> {
341 let json_value = serde_json::to_value(schema)
342 .map_err(|e| format!("Failed to serialize schemars schema: {}", e))?;
343
344 let obj = json_value
345 .as_object()
346 .ok_or_else(|| "Schema is not an object".to_string())?;
347
348 let is_object = obj.get("type").is_some_and(|v| {
350 v.as_str() == Some("object")
351 || v.as_array()
352 .is_some_and(|arr| arr.iter().any(|t| t.as_str() == Some("object")))
353 }) || obj.contains_key("properties");
354
355 if !is_object {
356 return Err("ToolSchema requires an object schema (type: \"object\")".to_string());
357 }
358
359 let mut definitions: HashMap<String, Value> = HashMap::new();
361 for key in ["$defs", "definitions"] {
362 if let Some(defs) = obj.get(key).and_then(|v| v.as_object()) {
363 definitions.extend(defs.iter().map(|(k, v)| (k.clone(), v.clone())));
364 }
365 }
366
367 let properties = obj
369 .get("properties")
370 .and_then(|v| v.as_object())
371 .map(|props| {
372 props
373 .iter()
374 .map(|(k, v)| {
375 (
376 k.clone(),
377 convert_value_to_json_schema_with_defs(v, &definitions),
378 )
379 })
380 .collect()
381 });
382
383 let required = obj
384 .get("required")
385 .and_then(|v| v.as_array())
386 .map(|arr| {
387 arr.iter()
388 .filter_map(|v| v.as_str().map(String::from))
389 .collect()
390 });
391
392 let reserved = [
394 "type",
395 "properties",
396 "required",
397 "$defs",
398 "definitions",
399 "$schema",
400 ];
401 let additional: HashMap<String, Value> = obj
402 .iter()
403 .filter(|(k, _)| !reserved.contains(&k.as_str()))
404 .map(|(k, v)| (k.clone(), v.clone()))
405 .collect();
406
407 Ok(ToolSchema {
408 schema_type: "object".to_string(),
409 properties,
410 required,
411 additional,
412 })
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use schemars::{JsonSchema, schema_for};
420 use serde::{Deserialize, Serialize};
421
422 #[derive(Serialize, JsonSchema)]
423 struct TestOutput {
424 value: i32,
425 message: String,
426 }
427
428 #[test]
429 fn test_from_schemars_basic() {
430 let json_schema = schema_for!(TestOutput);
431 let result = ToolSchema::from_schemars(json_schema);
432
433 assert!(result.is_ok(), "Schema conversion should succeed");
434 let tool_schema = result.unwrap();
435 assert_eq!(tool_schema.schema_type, "object");
436 }
437
438 #[test]
439 fn test_from_schemars_with_optional_field() {
440 #[derive(Serialize, Deserialize, JsonSchema)]
441 struct OutputWithOptional {
442 required_field: String,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 optional_field: Option<i32>,
445 }
446
447 let json_schema = schema_for!(OutputWithOptional);
448 let result = ToolSchema::from_schemars(json_schema);
449
450 assert!(
451 result.is_ok(),
452 "Schema with optional fields should convert successfully"
453 );
454 let schema = result.unwrap();
455 assert_eq!(schema.schema_type, "object");
456 assert!(schema.properties.is_some());
457 let props = schema.properties.as_ref().unwrap();
458 assert!(props.contains_key("required_field"));
459 assert!(props.contains_key("optional_field"));
460 }
461
462 #[test]
463 fn test_from_schemars_anyof_null() {
464 #[derive(Serialize, Deserialize, JsonSchema)]
465 struct Inner {
466 x: i32,
467 }
468
469 #[derive(Serialize, Deserialize, JsonSchema)]
470 struct WithOptionalNested {
471 name: String,
472 inner: Option<Inner>,
473 }
474
475 let json_schema = schema_for!(WithOptionalNested);
476 let result = ToolSchema::from_schemars(json_schema);
477
478 assert!(
479 result.is_ok(),
480 "Schema with anyOf/null optional nested struct should convert: {:?}",
481 result.err()
482 );
483 let schema = result.unwrap();
484 assert_eq!(schema.schema_type, "object");
485 let props = schema.properties.as_ref().unwrap();
486 assert!(props.contains_key("name"));
487 assert!(props.contains_key("inner"));
488 }
489
490 #[test]
491 fn test_from_schemars_with_nested_ref() {
492 #[derive(Serialize, Deserialize, JsonSchema)]
493 struct Nested {
494 value: f64,
495 }
496
497 #[derive(Serialize, Deserialize, JsonSchema)]
498 struct WithNested {
499 label: String,
500 nested: Nested,
501 }
502
503 let json_schema = schema_for!(WithNested);
504 let result = ToolSchema::from_schemars(json_schema);
505
506 assert!(
507 result.is_ok(),
508 "Schema with $ref nested struct should convert: {:?}",
509 result.err()
510 );
511 let schema = result.unwrap();
512 assert_eq!(schema.schema_type, "object");
513 let props = schema.properties.as_ref().unwrap();
514 assert!(props.contains_key("label"));
515 assert!(props.contains_key("nested"));
516 }
517
518 #[test]
519 fn test_from_schemars_with_legacy_definitions() {
520 let schema_json = serde_json::json!({
522 "type": "object",
523 "properties": {
524 "item": { "$ref": "#/definitions/Item" }
525 },
526 "required": ["item"],
527 "definitions": {
528 "Item": {
529 "type": "object",
530 "properties": {
531 "id": { "type": "integer" }
532 },
533 "required": ["id"]
534 }
535 }
536 });
537
538 let schema: schemars::Schema =
539 serde_json::from_value(schema_json).expect("valid schemars schema");
540 let result = ToolSchema::from_schemars(schema);
541
542 assert!(
543 result.is_ok(),
544 "Schema with legacy definitions should convert: {:?}",
545 result.err()
546 );
547 let tool_schema = result.unwrap();
548 assert_eq!(tool_schema.schema_type, "object");
549 let props = tool_schema.properties.as_ref().unwrap();
550 assert!(props.contains_key("item"));
551 }
552
553 #[test]
554 fn test_from_schemars_rejects_non_object() {
555 let json_schema = schema_for!(String);
556 let result = ToolSchema::from_schemars(json_schema);
557
558 assert!(
559 result.is_err(),
560 "Non-object root schema should be rejected"
561 );
562 assert!(result
563 .unwrap_err()
564 .contains("ToolSchema requires an object schema"));
565 }
566}