use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(transparent)]
pub struct InputSchema(
)` so the type stays closed to arbitrary external
pub(crate) Value,
);
impl Default for InputSchema {
#[inline]
fn default() -> Self {
Self(json!({
"type": "object",
"properties": {}
}))
}
}
impl InputSchema {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn as_value(&self) -> &Value {
&self.0
}
#[inline]
pub fn into_value(self) -> Value {
self.0
}
}
#[cfg(feature = "server")]
impl InputSchema {
#[inline]
pub fn from_value(value: Value) -> Self {
Self(value)
}
#[inline]
pub fn from_json_str(json: &str) -> Result<Self, crate::error::Error> {
let value: Value = serde_json::from_str(json)?;
Ok(Self(value))
}
#[inline]
pub fn from_schema<T: schemars::JsonSchema>() -> Self {
let json_schema = schemars::schema_for!(T);
Self::from_schemars(json_schema)
}
#[inline]
pub fn from_schemars(schema: schemars::Schema) -> Self {
Self(schema.to_value())
}
}
impl From<Value> for InputSchema {
#[inline]
fn from(value: Value) -> Self {
Self(value)
}
}
#[cfg(feature = "server")]
impl From<schemars::Schema> for InputSchema {
#[inline]
fn from(schema: schemars::Schema) -> Self {
Self(schema.to_value())
}
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
#[derive(Debug)]
pub struct SchemaProbe<T>(std::marker::PhantomData<T>);
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
impl<T> SchemaProbe<T> {
#[inline]
pub fn new() -> Self {
Self(std::marker::PhantomData)
}
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
impl<T> Default for SchemaProbe<T> {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
pub trait ViaJsonSchema {
fn neva_subschema(&self) -> Value;
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
impl<T: schemars::JsonSchema> ViaJsonSchema for &SchemaProbe<T> {
#[inline]
fn neva_subschema(&self) -> Value {
let settings =
schemars::generate::SchemaSettings::draft2020_12().with(|s| s.inline_subschemas = true);
schemars::SchemaGenerator::new(settings)
.into_root_schema_for::<T>()
.to_value()
}
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
pub trait ViaFallback {
fn neva_subschema(&self) -> Value;
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
impl<T> ViaFallback for SchemaProbe<T> {
#[inline]
fn neva_subschema(&self) -> Value {
json!({ "type": "object" })
}
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
#[inline]
pub fn primitive_subschema(ty: &str) -> Value {
json!({ "type": ty })
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
pub fn object_schema(properties: Vec<(String, Value)>, required: Vec<String>) -> InputSchema {
let mut props = serde_json::Map::with_capacity(properties.len());
for (name, schema) in properties {
props.insert(name, schema);
}
let mut root = serde_json::Map::with_capacity(3);
root.insert("type".to_string(), Value::String("object".to_string()));
root.insert("properties".to_string(), Value::Object(props));
root.insert(
"required".to_string(),
Value::Array(required.into_iter().map(Value::String).collect()),
);
InputSchema::from(Value::Object(root))
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[doc(hidden)]
#[macro_export]
macro_rules! __tool_arg_subschema {
($t:ty) => {{
#[allow(unused_imports)]
use $crate::__macro_support::{ViaFallback as _, ViaJsonSchema as _};
(&&$crate::__macro_support::SchemaProbe::<$t>::new()).neva_subschema()
}};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn default_is_empty_object_schema() {
let schema = InputSchema::default();
let value = serde_json::to_value(&schema).expect("serializes");
assert_eq!(
value,
json!({
"type": "object",
"properties": {}
})
);
let s = serde_json::to_string(&schema).expect("to_string");
let parsed: InputSchema = serde_json::from_str(&s).expect("from_str");
assert_eq!(parsed, schema);
}
#[test]
fn new_equals_default() {
assert_eq!(InputSchema::new(), InputSchema::default());
}
#[test]
fn transparent_serde_has_no_wrapper_key() {
let raw = json!({ "type": "string", "minLength": 1 });
let schema = InputSchema(raw.clone());
let serialized = serde_json::to_value(&schema).expect("serializes");
assert_eq!(serialized, raw);
let from_json: InputSchema = serde_json::from_value(raw.clone()).expect("from_value");
assert_eq!(from_json.as_value(), &raw);
}
#[test]
fn as_value_and_into_value_yield_inner() {
let raw = json!({ "type": "boolean" });
let schema = InputSchema(raw.clone());
assert_eq!(schema.as_value(), &raw);
assert_eq!(schema.into_value(), raw);
}
#[cfg(feature = "server")]
#[test]
fn from_json_str_invalid_returns_error() {
let result = InputSchema::from_json_str("{not valid json");
assert!(result.is_err(), "expected Err for malformed JSON");
}
#[cfg(feature = "server")]
#[test]
fn from_json_str_valid_round_trips() {
let input = r#"{"type":"object","properties":{"x":{"type":"integer"}},"required":["x"]}"#;
let schema = InputSchema::from_json_str(input).expect("valid JSON parses");
let re_serialized = serde_json::to_string(&schema).expect("serializes");
let reparsed: Value = serde_json::from_str(&re_serialized).expect("reparses");
let expected: Value = serde_json::from_str(input).expect("expected parses");
assert_eq!(reparsed, expected);
}
#[cfg(feature = "server")]
#[test]
fn from_value_preserves_arbitrary_keys() {
let raw = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/widget.schema.json",
"title": "Widget",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"kind": { "$ref": "#/$defs/kind" }
},
"required": ["id"],
"additionalProperties": false,
"oneOf": [
{ "required": ["id"] },
{ "required": ["kind"] }
],
"$defs": {
"kind": { "enum": ["a", "b", "c"] }
},
"x-custom-vendor-keyword": { "anything": [1, 2, 3] }
});
let schema = InputSchema::from_value(raw.clone());
assert_eq!(schema.as_value(), &raw);
let s = serde_json::to_string(&schema).expect("serializes");
let back: InputSchema = serde_json::from_str(&s).expect("deserializes");
assert_eq!(back.as_value(), &raw);
}
#[cfg(feature = "server")]
#[test]
fn from_impl_for_value_wraps_inner() {
let raw = json!({ "type": "null" });
let schema: InputSchema = raw.clone().into();
assert_eq!(schema.as_value(), &raw);
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[test]
fn probe_uses_schemars_when_type_derives_json_schema() {
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
let v = crate::__tool_arg_subschema!(Point);
assert_eq!(v["type"], json!("object"));
assert!(v["properties"].is_object(), "expected rich properties");
assert!(v["properties"]["x"].is_object());
assert!(
v.get("$defs").is_none(),
"subschema must be inlined (no $defs)"
);
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[test]
fn probe_falls_back_for_type_without_json_schema() {
struct Opaque;
let v = crate::__tool_arg_subschema!(Opaque);
assert_eq!(v, json!({ "type": "object" }));
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[test]
fn probe_inlines_nested_struct_without_refs() {
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct Inner {
label: String,
}
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct Outer {
inner: Inner,
}
let v = crate::__tool_arg_subschema!(Outer);
let s = serde_json::to_string(&v).unwrap();
assert!(
!s.contains("$ref"),
"inlined schema must not contain $ref: {s}"
);
assert!(
!s.contains("$defs"),
"inlined schema must not contain $defs: {s}"
);
assert!(v["properties"]["inner"]["properties"]["label"].is_object());
}
#[cfg(feature = "proto-2026-07-28-rc")]
#[test]
fn object_schema_assembles_properties_and_required() {
use crate::__macro_support::{object_schema, primitive_subschema};
let s = object_schema(
vec![
("a".to_string(), primitive_subschema("number")),
("b".to_string(), primitive_subschema("string")),
],
vec!["a".to_string(), "b".to_string()],
);
let v = s.into_value();
assert_eq!(v["type"], json!("object"));
assert_eq!(v["properties"]["a"]["type"], json!("number"));
assert_eq!(v["properties"]["b"]["type"], json!("string"));
let mut req: Vec<String> = serde_json::from_value(v["required"].clone()).unwrap();
req.sort();
assert_eq!(req, vec!["a".to_string(), "b".to_string()]);
}
}