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.get("required").and_then(|v| v.as_array()).map(|arr| {
384 arr.iter()
385 .filter_map(|v| v.as_str().map(String::from))
386 .collect()
387 });
388
389 let reserved = [
391 "type",
392 "properties",
393 "required",
394 "$defs",
395 "definitions",
396 "$schema",
397 ];
398 let additional: HashMap<String, Value> = obj
399 .iter()
400 .filter(|(k, _)| !reserved.contains(&k.as_str()))
401 .map(|(k, v)| (k.clone(), v.clone()))
402 .collect();
403
404 Ok(ToolSchema {
405 schema_type: "object".to_string(),
406 properties,
407 required,
408 additional,
409 })
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use schemars::{JsonSchema, schema_for};
417 use serde::{Deserialize, Serialize};
418
419 #[derive(Serialize, JsonSchema)]
420 struct TestOutput {
421 value: i32,
422 message: String,
423 }
424
425 #[test]
426 fn test_from_schemars_basic() {
427 let json_schema = schema_for!(TestOutput);
428 let result = ToolSchema::from_schemars(json_schema);
429
430 assert!(result.is_ok(), "Schema conversion should succeed");
431 let tool_schema = result.unwrap();
432 assert_eq!(tool_schema.schema_type, "object");
433 }
434
435 #[test]
436 fn test_from_schemars_with_optional_field() {
437 #[derive(Serialize, Deserialize, JsonSchema)]
438 struct OutputWithOptional {
439 required_field: String,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 optional_field: Option<i32>,
442 }
443
444 let json_schema = schema_for!(OutputWithOptional);
445 let result = ToolSchema::from_schemars(json_schema);
446
447 assert!(
448 result.is_ok(),
449 "Schema with optional fields should convert successfully"
450 );
451 let schema = result.unwrap();
452 assert_eq!(schema.schema_type, "object");
453 assert!(schema.properties.is_some());
454 let props = schema.properties.as_ref().unwrap();
455 assert!(props.contains_key("required_field"));
456 assert!(props.contains_key("optional_field"));
457 }
458
459 #[test]
460 fn test_from_schemars_anyof_null() {
461 #[derive(Serialize, Deserialize, JsonSchema)]
462 struct Inner {
463 x: i32,
464 }
465
466 #[derive(Serialize, Deserialize, JsonSchema)]
467 struct WithOptionalNested {
468 name: String,
469 inner: Option<Inner>,
470 }
471
472 let json_schema = schema_for!(WithOptionalNested);
473 let result = ToolSchema::from_schemars(json_schema);
474
475 assert!(
476 result.is_ok(),
477 "Schema with anyOf/null optional nested struct should convert: {:?}",
478 result.err()
479 );
480 let schema = result.unwrap();
481 assert_eq!(schema.schema_type, "object");
482 let props = schema.properties.as_ref().unwrap();
483 assert!(props.contains_key("name"));
484 assert!(props.contains_key("inner"));
485 }
486
487 #[test]
488 fn test_from_schemars_with_nested_ref() {
489 #[derive(Serialize, Deserialize, JsonSchema)]
490 struct Nested {
491 value: f64,
492 }
493
494 #[derive(Serialize, Deserialize, JsonSchema)]
495 struct WithNested {
496 label: String,
497 nested: Nested,
498 }
499
500 let json_schema = schema_for!(WithNested);
501 let result = ToolSchema::from_schemars(json_schema);
502
503 assert!(
504 result.is_ok(),
505 "Schema with $ref nested struct should convert: {:?}",
506 result.err()
507 );
508 let schema = result.unwrap();
509 assert_eq!(schema.schema_type, "object");
510 let props = schema.properties.as_ref().unwrap();
511 assert!(props.contains_key("label"));
512 assert!(props.contains_key("nested"));
513 }
514
515 #[test]
516 fn test_from_schemars_with_legacy_definitions() {
517 let schema_json = serde_json::json!({
519 "type": "object",
520 "properties": {
521 "item": { "$ref": "#/definitions/Item" }
522 },
523 "required": ["item"],
524 "definitions": {
525 "Item": {
526 "type": "object",
527 "properties": {
528 "id": { "type": "integer" }
529 },
530 "required": ["id"]
531 }
532 }
533 });
534
535 let schema: schemars::Schema =
536 serde_json::from_value(schema_json).expect("valid schemars schema");
537 let result = ToolSchema::from_schemars(schema);
538
539 assert!(
540 result.is_ok(),
541 "Schema with legacy definitions should convert: {:?}",
542 result.err()
543 );
544 let tool_schema = result.unwrap();
545 assert_eq!(tool_schema.schema_type, "object");
546 let props = tool_schema.properties.as_ref().unwrap();
547 assert!(props.contains_key("item"));
548 }
549
550 #[test]
551 fn test_from_schemars_rejects_non_object() {
552 let json_schema = schema_for!(String);
553 let result = ToolSchema::from_schemars(json_schema);
554
555 assert!(result.is_err(), "Non-object root schema should be rejected");
556 assert!(
557 result
558 .unwrap_err()
559 .contains("ToolSchema requires an object schema")
560 );
561 }
562}