use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseFormat {
Text,
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
json_schema: JsonSchemaSpec,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JsonSchemaSpec {
pub name: String,
pub schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl ResponseFormat {
pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
ResponseFormat::JsonSchema {
json_schema: JsonSchemaSpec {
name: name.into(),
schema,
strict: Some(true),
},
}
}
}
pub fn make_strict_schema(schema: &mut serde_json::Value) {
let Some(obj) = schema.as_object_mut() else {
return;
};
if obj.contains_key("properties") {
obj.insert(
"additionalProperties".to_string(),
serde_json::Value::Bool(false),
);
let mut required: Vec<serde_json::Value> = Vec::new();
if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
for name in props.keys() {
required.push(serde_json::Value::String(name.clone()));
}
for value in props.values_mut() {
make_strict_schema(value);
}
}
obj.insert("required".to_string(), serde_json::Value::Array(required));
}
for key in ["items", "additionalProperties", "not"] {
if let Some(value) = obj.get_mut(key) {
if value.is_object() {
make_strict_schema(value);
}
}
}
for key in ["anyOf", "oneOf", "allOf", "prefixItems"] {
if let Some(list) = obj.get_mut(key).and_then(|l| l.as_array_mut()) {
for value in list.iter_mut() {
make_strict_schema(value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_response_format_serialization_text() {
let value = serde_json::to_value(ResponseFormat::Text).unwrap();
assert_eq!(value, json!({"type": "text"}));
}
#[test]
fn test_response_format_serialization_json_object() {
let value = serde_json::to_value(ResponseFormat::JsonObject).unwrap();
assert_eq!(value, json!({"type": "json_object"}));
}
#[test]
fn test_response_format_serialization_json_schema() {
let format = ResponseFormat::json_schema("person", json!({"type": "object"}));
let value = serde_json::to_value(&format).unwrap();
assert_eq!(
value,
json!({
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {"type": "object"},
"strict": true
}
})
);
}
#[test]
fn test_make_strict_schema_top_level() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name"],
"additionalProperties": true
});
make_strict_schema(&mut schema);
assert_eq!(schema["additionalProperties"], json!(false));
let required: Vec<&str> = schema["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(required, vec!["age", "name"]);
}
#[test]
fn test_make_strict_schema_recursive() {
let mut schema = json!({
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {"label": {"type": "string"}}
}
}
}
});
make_strict_schema(&mut schema);
assert_eq!(schema["additionalProperties"], json!(false));
assert_eq!(schema["required"], json!(["tags"]));
let items = &schema["properties"]["tags"]["items"];
assert_eq!(items["additionalProperties"], json!(false));
assert_eq!(items["required"], json!(["label"]));
}
#[test]
fn test_make_strict_schema_variants() {
let mut schema = json!({
"type": "object",
"properties": {
"value": {
"anyOf": [
{"type": "object", "properties": {"a": {"type": "string"}}},
{"type": "object", "properties": {"b": {"type": "integer"}}}
]
}
}
});
make_strict_schema(&mut schema);
let branches = &schema["properties"]["value"]["anyOf"];
assert_eq!(branches[0]["additionalProperties"], json!(false));
assert_eq!(branches[1]["additionalProperties"], json!(false));
}
#[test]
fn test_make_strict_schema_non_object() {
let mut schema = json!({"type": "string", "enum": ["a", "b"]});
make_strict_schema(&mut schema);
assert_eq!(schema, json!({"type": "string", "enum": ["a", "b"]}));
let mut scalar = json!("just a string");
make_strict_schema(&mut scalar);
assert_eq!(scalar, json!("just a string"));
}
#[test]
fn test_make_strict_schema_idempotent() {
let mut schema = json!({
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
"additionalProperties": false
});
make_strict_schema(&mut schema);
let once = schema.clone();
make_strict_schema(&mut schema);
assert_eq!(schema, once);
}
}