codex_app_server_sdk/
schema.rs1use 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}