Skip to main content

codex_app_server_sdk/
schema.rs

1use schemars::JsonSchema;
2use schemars::generate::SchemaSettings;
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7pub trait OpenAiSerializable: Sized {
8    fn openai_output_schema() -> Value;
9
10    fn to_openai_value(&self) -> serde_json::Result<Value>
11    where
12        Self: Serialize,
13    {
14        serialize_openai_value(self)
15    }
16
17    fn from_openai_value(value: Value) -> serde_json::Result<Self>
18    where
19        Self: DeserializeOwned,
20    {
21        deserialize_openai_value(value)
22    }
23}
24
25pub fn openai_json_schema_for<T>() -> Value
26where
27    T: JsonSchema,
28{
29    let schema = SchemaSettings::default()
30        .with(|settings| settings.meta_schema = None)
31        .into_generator()
32        .into_root_schema_for::<T>();
33    let mut schema =
34        serde_json::to_value(schema).expect("serializing generated schema should not fail");
35    enforce_openai_object_constraints(&mut schema);
36    schema
37}
38
39pub fn serialize_openai_value<T>(value: &T) -> serde_json::Result<Value>
40where
41    T: Serialize,
42{
43    serde_json::to_value(value)
44}
45
46pub fn deserialize_openai_value<T>(value: Value) -> serde_json::Result<T>
47where
48    T: DeserializeOwned,
49{
50    serde_json::from_value(value)
51}
52
53fn enforce_openai_object_constraints(value: &mut Value) {
54    match value {
55        Value::Object(map) => {
56            for nested in map.values_mut() {
57                enforce_openai_object_constraints(nested);
58            }
59
60            let is_object_schema = map.get("type").and_then(Value::as_str) == Some("object");
61            if is_object_schema && !map.contains_key("additionalProperties") {
62                map.insert("additionalProperties".to_string(), Value::Bool(false));
63            }
64        }
65        Value::Array(values) => {
66            for nested in values {
67                enforce_openai_object_constraints(nested);
68            }
69        }
70        _ => {}
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use schemars::JsonSchema;
78    use serde::{Deserialize, Serialize};
79
80    #[derive(
81        Debug,
82        Clone,
83        PartialEq,
84        Eq,
85        Serialize,
86        Deserialize,
87        JsonSchema,
88        codex_app_server_sdk::OpenAiSerializable,
89    )]
90    struct SummarySchema {
91        answer: String,
92    }
93
94    #[test]
95    fn derives_openai_schema_without_meta_schema() {
96        let schema = SummarySchema::openai_output_schema();
97        assert!(schema.get("$schema").is_none());
98        assert_eq!(
99            schema.get("type"),
100            Some(&Value::String("object".to_string()))
101        );
102        assert_eq!(
103            schema.get("additionalProperties"),
104            Some(&Value::Bool(false))
105        );
106        let properties = schema
107            .get("properties")
108            .and_then(Value::as_object)
109            .expect("schema should include properties object");
110        assert!(properties.contains_key("answer"));
111    }
112
113    #[test]
114    fn helper_serializes_and_deserializes_struct_values() {
115        let expected = SummarySchema {
116            answer: "ok".to_string(),
117        };
118        let value = expected
119            .to_openai_value()
120            .expect("serialize should succeed");
121        let parsed = SummarySchema::from_openai_value(value).expect("deserialize should succeed");
122        assert_eq!(parsed, expected);
123    }
124
125    #[test]
126    fn schema_helper_accepts_json_schema_types_directly() {
127        let schema = openai_json_schema_for::<SummarySchema>();
128        assert_eq!(
129            schema.get("type"),
130            Some(&Value::String("object".to_string()))
131        );
132    }
133}