Skip to main content

codex_app_server_sdk/
schema.rs

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