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