feat: Better typing for JSON Schema
- [x] I have looked for existing issues (including closed) about this
Feature Request
Right now the schemas are just serde_json::Value. I am wondering if it would be worth either building primitives to better type that or at least verify that this is a valid JSON Schema.
Motivation
The JSON value is very barebone and doesn't offer a lot of security/guard rails for the user. It is even worse if you have to accept user provided data for tools. That would also allow us to transform the schema if need be for each provider, though most of them seem to support part of the JSON Schema (at whatever draft version it is at right now).
Proposal
- We could start with a custom wrapper around the Value that would check with jsonschema that it is a valid JSON Schema
- We could build/contribute to an existing crate a macro/builder for JSON Schema and a parser for it (similar to what valico offers
Alternatives
- The user needs to do it's own parsing
Hi, I'd like to work on this
I came up with with this for a wrapper around Value that adds validation, what do you think about the approach ?
use serde_json::Value;
#[derive(Debug)]
enum ValidationError {
ValidationError(String),
SerdeError(serde_json::Error),
}
trait Schema {
fn schema() -> String;
fn validate(value: &Value) -> Result<(), ValidationError> {
let schema = serde_json::from_str::<Value>(&Self::schema())
.map_err(|e| ValidationError::SerdeError(e))?;
jsonschema::validate(&schema, value)
.map_err(|e| ValidationError::ValidationError(e.to_string()))?;
Ok(())
}
}
struct ValidatedValue<T: Schema> {
value: Value,
marker: std::marker::PhantomData<T>,
}
impl<T: Schema> TryFrom<Value> for ValidatedValue<T> {
type Error = ValidationError;
fn try_from(value: Value) -> Result<Self, Self::Error> {
T::validate(&value)?;
Ok(ValidatedValue {
value,
marker: std::marker::PhantomData,
})
}
}
you'd define a schema like this
struct ToolUseParameters;
impl Schema for ToolUseParameters {
fn schema() -> String {
r#"
{
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" }
},
"required": ["name", "description"]
}
"#
.to_string()
}
}
struct ToolDefinition {
name: String,
parameters: ValidatedValue<ToolUseParameters>,
}
and then use it like this
fn test() {
Definition {
name: "Example Tool".to_string(),
parameters: json!({
"name": "Add",
"description": "add stuff"
})
.try_into()
.unwrap(),
};
}
I think doing it this way still keeps the interface really clean for the user and makes it easy to define and reuse schemas. we could also potentially use a macro to make things even simpler for users
macro_rules! validated_json {
($($value:tt)+) => {
json!($($value)+).try_into().unwrap()
};
}
validated_json!({
"name": "Add",
"description": "add stuff"
})
My initial thought is that goal is to have a proper DSL so you don't have to know the structure of a json schema to build one, this doesn't really help for that.
The output likely needs to a wrapper over a serde_json::Value with validation and a custom deserializer for it.
oh I see, I was headed in the wrong direction then, what do you think about this for proposal 1
struct Schema(Value);
impl TryFrom<Value> for Schema {
type Error = ValidationError<'static>;
fn try_from(value: Value) -> Result<Self, Self::Error> {
jsonschema::validator_for(&value)?;
Ok(Schema(value))
}
}
also, what kind of structure do you have in mind for the DSL (sorry if i'm asking too many questions)
I think this might actually be a job better suited for schemars, since we already use it internally.
What do you think?