Skip to main content

codex_tools/
json_schema.rs

1use serde::Deserialize;
2use serde::Serialize;
3use serde_json::Value as JsonValue;
4use serde_json::json;
5use std::collections::BTreeMap;
6use std::collections::BTreeSet;
7
8const DEFINITION_TABLE_KEYS: [&str; 2] = ["$defs", "definitions"];
9const SCHEMA_CHILD_KEYS: [&str; 4] = ["items", "anyOf", "oneOf", "allOf"];
10const COMPOSITION_SCHEMA_KEYS: [&str; 3] = ["anyOf", "oneOf", "allOf"];
11
12/// Primitive JSON Schema type names we support in tool definitions.
13///
14/// This mirrors the OpenAI Structured Outputs subset for JSON Schema `type`:
15/// string, number, boolean, integer, object, array, and null.
16/// Keywords such as `enum`, `const`, `anyOf`, `oneOf`, and `allOf` are modeled
17/// separately.
18/// See <https://developers.openai.com/api/docs/guides/structured-outputs#supported-schemas>.
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "lowercase")]
21pub enum JsonSchemaPrimitiveType {
22    String,
23    Number,
24    Boolean,
25    Integer,
26    Object,
27    Array,
28    Null,
29}
30
31/// JSON Schema `type` supports either a single type name or a union of names.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(untagged)]
34pub enum JsonSchemaType {
35    Single(JsonSchemaPrimitiveType),
36    Multiple(Vec<JsonSchemaPrimitiveType>),
37}
38
39/// Generic JSON-Schema subset needed for our tool definitions.
40#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
41pub struct JsonSchema {
42    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
43    pub schema_ref: Option<String>,
44    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
45    pub schema_type: Option<JsonSchemaType>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub description: Option<String>,
48    /// Responses-only marker for reviewed encrypted tool parameters.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub encrypted: Option<bool>,
51    #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
52    pub enum_values: Option<Vec<JsonValue>>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub items: Option<Box<JsonSchema>>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub properties: Option<BTreeMap<String, JsonSchema>>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub required: Option<Vec<String>>,
59    #[serde(
60        rename = "additionalProperties",
61        skip_serializing_if = "Option::is_none"
62    )]
63    pub additional_properties: Option<AdditionalProperties>,
64    #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
65    pub any_of: Option<Vec<JsonSchema>>,
66    #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
67    pub one_of: Option<Vec<JsonSchema>>,
68    #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
69    pub all_of: Option<Vec<JsonSchema>>,
70    #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
71    pub defs: Option<BTreeMap<String, JsonSchema>>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub definitions: Option<BTreeMap<String, JsonSchema>>,
74}
75
76impl JsonSchema {
77    /// Construct a scalar/object/array schema with a single JSON Schema type.
78    fn typed(schema_type: JsonSchemaPrimitiveType, description: Option<String>) -> Self {
79        Self {
80            schema_type: Some(JsonSchemaType::Single(schema_type)),
81            description,
82            ..Default::default()
83        }
84    }
85
86    pub fn any_of(variants: Vec<JsonSchema>, description: Option<String>) -> Self {
87        Self {
88            description,
89            any_of: Some(variants),
90            ..Default::default()
91        }
92    }
93
94    pub fn one_of(variants: Vec<JsonSchema>, description: Option<String>) -> Self {
95        Self {
96            description,
97            one_of: Some(variants),
98            ..Default::default()
99        }
100    }
101
102    pub fn all_of(variants: Vec<JsonSchema>, description: Option<String>) -> Self {
103        Self {
104            description,
105            all_of: Some(variants),
106            ..Default::default()
107        }
108    }
109
110    pub fn boolean(description: Option<String>) -> Self {
111        Self::typed(JsonSchemaPrimitiveType::Boolean, description)
112    }
113
114    pub fn string(description: Option<String>) -> Self {
115        Self::typed(JsonSchemaPrimitiveType::String, description)
116    }
117
118    pub fn with_encrypted(mut self) -> Self {
119        self.encrypted = Some(true);
120        self
121    }
122
123    pub fn number(description: Option<String>) -> Self {
124        Self::typed(JsonSchemaPrimitiveType::Number, description)
125    }
126
127    pub fn integer(description: Option<String>) -> Self {
128        Self::typed(JsonSchemaPrimitiveType::Integer, description)
129    }
130
131    pub fn null(description: Option<String>) -> Self {
132        Self::typed(JsonSchemaPrimitiveType::Null, description)
133    }
134
135    pub fn string_enum(values: Vec<JsonValue>, description: Option<String>) -> Self {
136        Self {
137            schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::String)),
138            description,
139            enum_values: Some(values),
140            ..Default::default()
141        }
142    }
143
144    pub fn array(items: JsonSchema, description: Option<String>) -> Self {
145        Self {
146            schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Array)),
147            description,
148            items: Some(Box::new(items)),
149            ..Default::default()
150        }
151    }
152
153    pub fn object(
154        properties: BTreeMap<String, JsonSchema>,
155        required: Option<Vec<String>>,
156        additional_properties: Option<AdditionalProperties>,
157    ) -> Self {
158        Self {
159            schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
160            properties: Some(properties),
161            required,
162            additional_properties,
163            ..Default::default()
164        }
165    }
166}
167
168/// Whether additional properties are allowed, and if so, any required schema.
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170#[serde(untagged)]
171pub enum AdditionalProperties {
172    Boolean(bool),
173    Schema(Box<JsonSchema>),
174}
175
176impl From<bool> for AdditionalProperties {
177    fn from(value: bool) -> Self {
178        Self::Boolean(value)
179    }
180}
181
182impl From<JsonSchema> for AdditionalProperties {
183    fn from(value: JsonSchema) -> Self {
184        Self::Schema(Box::new(value))
185    }
186}
187
188/// Parse the tool `input_schema` or return an error for invalid schema.
189pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, serde_json::Error> {
190    let mut input_schema = prepare_tool_input_schema(input_schema);
191    compact_large_tool_schema(&mut input_schema);
192    deserialize_tool_input_schema(input_schema)
193}
194
195/// Parse a trusted tool `input_schema` without running large-schema compaction.
196pub fn parse_tool_input_schema_without_compaction(
197    input_schema: &JsonValue,
198) -> Result<JsonSchema, serde_json::Error> {
199    deserialize_tool_input_schema(prepare_tool_input_schema(input_schema))
200}
201
202fn prepare_tool_input_schema(input_schema: &JsonValue) -> JsonValue {
203    let mut input_schema = input_schema.clone();
204    sanitize_json_schema(&mut input_schema);
205    prune_unreachable_definitions(&mut input_schema);
206    input_schema
207}
208
209fn deserialize_tool_input_schema(input_schema: JsonValue) -> Result<JsonSchema, serde_json::Error> {
210    let schema: JsonSchema = serde_json::from_value(input_schema)?;
211    if matches!(
212        schema.schema_type,
213        Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Null))
214    ) {
215        return Err(singleton_null_schema_error());
216    }
217    Ok(schema)
218}
219
220// Use compact normalized JSON bytes as a cheap local proxy for the 1k-token
221// schema budget.
222const MAX_COMPACT_TOOL_SCHEMA_BYTES: usize = 5_000;
223const MAX_COMPACT_TOOL_SCHEMA_DEPTH: usize = 3;
224
225/// Shrink unusually large tool schemas while preserving the top-level argument
226/// surface. Compaction is best-effort rather than a hard cap: it runs only
227/// after schema sanitization/pruning and applies increasingly lossy passes
228/// while the schema remains over budget.
229fn compact_large_tool_schema(value: &mut JsonValue) {
230    for pass in LARGE_SCHEMA_COMPACTION_PASSES {
231        if compact_schema_fits_budget(value) {
232            break;
233        }
234        pass(value);
235    }
236}
237
238type LargeSchemaCompactionPass = fn(&mut JsonValue);
239
240const LARGE_SCHEMA_COMPACTION_PASSES: &[LargeSchemaCompactionPass] = &[
241    strip_schema_descriptions,
242    drop_schema_definitions,
243    collapse_deep_schema_objects_from_root,
244    prune_schema_compositions,
245];
246
247fn collapse_deep_schema_objects_from_root(value: &mut JsonValue) {
248    collapse_deep_schema_objects(value, /*depth*/ 0);
249}
250
251fn compact_schema_fits_budget(value: &JsonValue) -> bool {
252    compact_normalized_schema_len(value) <= MAX_COMPACT_TOOL_SCHEMA_BYTES
253}
254
255fn compact_normalized_schema_len(value: &JsonValue) -> usize {
256    serde_json::from_value::<JsonSchema>(value.clone())
257        .and_then(|schema| serde_json::to_vec(&schema))
258        .map(|json| json.len())
259        .unwrap_or(0)
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263enum DefinitionTraversal {
264    Include,
265    Skip,
266}
267
268fn for_each_schema_child(
269    map: &serde_json::Map<String, JsonValue>,
270    definition_traversal: DefinitionTraversal,
271    visitor: &mut impl FnMut(&JsonValue),
272) {
273    if let Some(properties) = map.get("properties")
274        && let Some(properties_map) = properties.as_object()
275    {
276        for value in properties_map.values() {
277            visitor(value);
278        }
279    }
280
281    for key in SCHEMA_CHILD_KEYS {
282        if let Some(value) = map.get(key) {
283            visitor(value);
284        }
285    }
286
287    if let Some(additional_properties) = map.get("additionalProperties")
288        && !matches!(additional_properties, JsonValue::Bool(_))
289    {
290        visitor(additional_properties);
291    }
292
293    if definition_traversal == DefinitionTraversal::Include {
294        for key in DEFINITION_TABLE_KEYS {
295            if let Some(definitions) = map.get(key)
296                && let Some(definitions_map) = definitions.as_object()
297            {
298                for value in definitions_map.values() {
299                    visitor(value);
300                }
301            }
302        }
303    }
304}
305
306fn strip_schema_descriptions(value: &mut JsonValue) {
307    match value {
308        JsonValue::Array(values) => {
309            for value in values {
310                strip_schema_descriptions(value);
311            }
312        }
313        JsonValue::Object(map) => {
314            map.remove("description");
315            for_each_schema_child_mut(map, DefinitionTraversal::Include, &mut |value| {
316                strip_schema_descriptions(value);
317            });
318        }
319        _ => {}
320    }
321}
322
323fn for_each_schema_child_mut(
324    map: &mut serde_json::Map<String, JsonValue>,
325    definition_traversal: DefinitionTraversal,
326    visitor: &mut impl FnMut(&mut JsonValue),
327) {
328    if let Some(properties) = map.get_mut("properties")
329        && let Some(properties_map) = properties.as_object_mut()
330    {
331        for value in properties_map.values_mut() {
332            visitor(value);
333        }
334    }
335
336    for key in SCHEMA_CHILD_KEYS {
337        if let Some(value) = map.get_mut(key) {
338            visitor(value);
339        }
340    }
341
342    if let Some(additional_properties) = map.get_mut("additionalProperties")
343        && !matches!(additional_properties, JsonValue::Bool(_))
344    {
345        visitor(additional_properties);
346    }
347
348    if definition_traversal == DefinitionTraversal::Include {
349        for key in DEFINITION_TABLE_KEYS {
350            if let Some(definitions) = map.get_mut(key)
351                && let Some(definitions_map) = definitions.as_object_mut()
352            {
353                for value in definitions_map.values_mut() {
354                    visitor(value);
355                }
356            }
357        }
358    }
359}
360
361/// Replace local definition refs with empty schemas before dropping root
362/// definition tables, so downstream behavior does not depend on how a schema
363/// parser handles refs to missing definitions.
364fn drop_schema_definitions(value: &mut JsonValue) {
365    rewrite_definition_refs_to_empty_schemas(value);
366
367    let JsonValue::Object(map) = value else {
368        return;
369    };
370
371    for key in DEFINITION_TABLE_KEYS {
372        map.remove(key);
373    }
374}
375
376fn rewrite_definition_refs_to_empty_schemas(value: &mut JsonValue) {
377    match value {
378        JsonValue::Array(values) => {
379            for value in values {
380                rewrite_definition_refs_to_empty_schemas(value);
381            }
382        }
383        JsonValue::Object(map) => {
384            if map
385                .get("$ref")
386                .and_then(JsonValue::as_str)
387                .and_then(parse_local_definition_ref)
388                .is_some()
389            {
390                *value = json!({});
391                return;
392            }
393
394            for_each_schema_child_mut(map, DefinitionTraversal::Skip, &mut |value| {
395                rewrite_definition_refs_to_empty_schemas(value);
396            });
397        }
398        _ => {}
399    }
400}
401
402fn collapse_deep_schema_objects(value: &mut JsonValue, depth: usize) {
403    match value {
404        JsonValue::Array(values) => {
405            for value in values {
406                collapse_deep_schema_objects(value, depth);
407            }
408        }
409        JsonValue::Object(map) => {
410            if depth >= MAX_COMPACT_TOOL_SCHEMA_DEPTH && is_complex_schema_object(map) {
411                *value = json!({});
412                return;
413            }
414
415            for_each_schema_child_mut(map, DefinitionTraversal::Skip, &mut |value| {
416                collapse_deep_schema_objects(value, depth + 1);
417            });
418        }
419        _ => {}
420    }
421}
422
423fn prune_schema_compositions(value: &mut JsonValue) {
424    match value {
425        JsonValue::Array(values) => {
426            for value in values {
427                prune_schema_compositions(value);
428            }
429        }
430        JsonValue::Object(map) => {
431            if has_composition_keyword(map) {
432                *value = json!({});
433                return;
434            }
435
436            for_each_schema_child_mut(map, DefinitionTraversal::Skip, &mut |value| {
437                prune_schema_compositions(value);
438            });
439        }
440        _ => {}
441    }
442}
443
444fn is_complex_schema_object(map: &serde_json::Map<String, JsonValue>) -> bool {
445    SCHEMA_CHILD_KEYS.iter().any(|key| map.contains_key(*key))
446        || map.contains_key("properties")
447        || map.contains_key("additionalProperties")
448        || map.contains_key("$ref")
449}
450
451fn has_composition_keyword(map: &serde_json::Map<String, JsonValue>) -> bool {
452    COMPOSITION_SCHEMA_KEYS
453        .into_iter()
454        .any(|key| map.contains_key(key))
455}
456
457/// Sanitize a JSON Schema (as serde_json::Value) so it can fit our limited
458/// schema representation. This function:
459/// - Ensures every typed schema object has a `"type"` when required.
460/// - Preserves explicit `anyOf`, `oneOf`, and `allOf`.
461/// - Preserves `$ref` and reachable local `$defs` / `definitions`.
462/// - Collapses `const` into single-value `enum`.
463/// - Fills required child fields for object/array schema types, including
464///   nullable unions, with permissive defaults when absent.
465/// - Coerces object schemas with no recognized schema hints into `{}`.
466fn sanitize_json_schema(value: &mut JsonValue) {
467    match value {
468        JsonValue::Bool(_) => {
469            // JSON Schema boolean form: true/false. Coerce to an accept-all string.
470            *value = json!({ "type": "string" });
471        }
472        JsonValue::Array(values) => {
473            for value in values {
474                sanitize_json_schema(value);
475            }
476        }
477        JsonValue::Object(map) => {
478            if let Some(properties) = map.get_mut("properties")
479                && let Some(properties_map) = properties.as_object_mut()
480            {
481                for value in properties_map.values_mut() {
482                    sanitize_json_schema(value);
483                }
484            }
485            if let Some(items) = map.get_mut("items") {
486                sanitize_json_schema(items);
487            }
488            if let Some(additional_properties) = map.get_mut("additionalProperties")
489                && !matches!(additional_properties, JsonValue::Bool(_))
490            {
491                sanitize_json_schema(additional_properties);
492            }
493            if let Some(value) = map.get_mut("prefixItems") {
494                sanitize_json_schema(value);
495            }
496            for key in COMPOSITION_SCHEMA_KEYS {
497                if let Some(value) = map.get_mut(key) {
498                    sanitize_json_schema(value);
499                }
500            }
501            for table in DEFINITION_TABLE_KEYS {
502                sanitize_schema_table(map, table);
503            }
504
505            if let Some(const_value) = map.remove("const") {
506                map.insert("enum".to_string(), JsonValue::Array(vec![const_value]));
507            }
508
509            let mut schema_types = normalized_schema_types(map);
510
511            if schema_types.is_empty() && (map.contains_key("$ref") || has_composition_keyword(map))
512            {
513                return;
514            }
515
516            if schema_types.is_empty() {
517                if map.contains_key("properties")
518                    || map.contains_key("required")
519                    || map.contains_key("additionalProperties")
520                {
521                    schema_types.push(JsonSchemaPrimitiveType::Object);
522                } else if map.contains_key("items") || map.contains_key("prefixItems") {
523                    schema_types.push(JsonSchemaPrimitiveType::Array);
524                } else if map.contains_key("enum") || map.contains_key("format") {
525                    schema_types.push(JsonSchemaPrimitiveType::String);
526                } else if map.contains_key("minimum")
527                    || map.contains_key("maximum")
528                    || map.contains_key("exclusiveMinimum")
529                    || map.contains_key("exclusiveMaximum")
530                    || map.contains_key("multipleOf")
531                {
532                    schema_types.push(JsonSchemaPrimitiveType::Number);
533                } else {
534                    map.clear();
535                    return;
536                }
537            }
538
539            write_schema_types(map, &schema_types);
540            ensure_default_children_for_schema_types(map, &schema_types);
541        }
542        _ => {}
543    }
544}
545
546/// Sanitize a schema definition table before deserializing into `JsonSchema`.
547///
548/// Definition tables must be objects. Codex keeps valid definition tables and
549/// recursively applies the same compatibility lowering used for inline schemas,
550/// but drops malformed tables so `strict: false` tool registration degrades
551/// gracefully instead of failing on an unreachable or invalid definition table.
552fn sanitize_schema_table(map: &mut serde_json::Map<String, JsonValue>, key: &str) {
553    let should_remove = match map.get_mut(key) {
554        Some(JsonValue::Object(definitions)) => {
555            for definition in definitions.values_mut() {
556                sanitize_json_schema(definition);
557            }
558            false
559        }
560        Some(_) => true,
561        None => false,
562    };
563
564    if should_remove {
565        map.remove(key);
566    }
567}
568
569fn ensure_default_children_for_schema_types(
570    map: &mut serde_json::Map<String, JsonValue>,
571    schema_types: &[JsonSchemaPrimitiveType],
572) {
573    if schema_types.contains(&JsonSchemaPrimitiveType::Object) && !map.contains_key("properties") {
574        map.insert(
575            "properties".to_string(),
576            JsonValue::Object(serde_json::Map::new()),
577        );
578    }
579
580    if schema_types.contains(&JsonSchemaPrimitiveType::Array) && !map.contains_key("items") {
581        map.insert("items".to_string(), json!({ "type": "string" }));
582    }
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
586struct DefinitionPointer {
587    table: &'static str,
588    name: String,
589}
590
591/// Prune unused root definition entries to avoid sending tokens for definitions
592/// the tool schema never references.
593fn prune_unreachable_definitions(value: &mut JsonValue) {
594    let reachable = collect_reachable_definitions(value);
595    let JsonValue::Object(map) = value else {
596        return;
597    };
598
599    for table in DEFINITION_TABLE_KEYS {
600        prune_schema_table(map, table, &reachable);
601    }
602}
603
604fn prune_schema_table(
605    map: &mut serde_json::Map<String, JsonValue>,
606    table: &'static str,
607    reachable: &BTreeSet<DefinitionPointer>,
608) {
609    let Some(JsonValue::Object(definitions)) = map.get_mut(table) else {
610        return;
611    };
612
613    definitions.retain(|name, _| {
614        reachable.contains(&DefinitionPointer {
615            table,
616            name: name.clone(),
617        })
618    });
619
620    if definitions.is_empty() {
621        map.remove(table);
622    }
623}
624
625fn collect_reachable_definitions(value: &JsonValue) -> BTreeSet<DefinitionPointer> {
626    let mut reachable = BTreeSet::new();
627    let mut pending = Vec::new();
628
629    collect_refs_outside_definitions(value, &mut pending);
630
631    while let Some(pointer) = pending.pop() {
632        if !reachable.insert(pointer.clone()) {
633            continue;
634        }
635
636        if let Some(definition) = definition_for_pointer(value, &pointer) {
637            collect_refs(definition, &mut pending);
638        }
639    }
640
641    reachable
642}
643
644fn collect_refs_outside_definitions(value: &JsonValue, refs: &mut Vec<DefinitionPointer>) {
645    match value {
646        JsonValue::Array(values) => {
647            for value in values {
648                collect_refs_outside_definitions(value, refs);
649            }
650        }
651        JsonValue::Object(map) => {
652            collect_ref_from_map(map, refs);
653            for_each_schema_child(map, DefinitionTraversal::Skip, &mut |value| {
654                collect_refs_outside_definitions(value, refs);
655            });
656        }
657        _ => {}
658    }
659}
660
661fn collect_refs(value: &JsonValue, refs: &mut Vec<DefinitionPointer>) {
662    match value {
663        JsonValue::Array(values) => {
664            for value in values {
665                collect_refs(value, refs);
666            }
667        }
668        JsonValue::Object(map) => {
669            collect_ref_from_map(map, refs);
670            for value in map.values() {
671                collect_refs(value, refs);
672            }
673        }
674        _ => {}
675    }
676}
677
678fn collect_ref_from_map(
679    map: &serde_json::Map<String, JsonValue>,
680    refs: &mut Vec<DefinitionPointer>,
681) {
682    if let Some(JsonValue::String(schema_ref)) = map.get("$ref")
683        && let Some(pointer) = parse_local_definition_ref(schema_ref)
684    {
685        refs.push(pointer);
686    }
687}
688
689fn definition_for_pointer<'a>(
690    value: &'a JsonValue,
691    pointer: &DefinitionPointer,
692) -> Option<&'a JsonValue> {
693    let JsonValue::Object(map) = value else {
694        return None;
695    };
696
697    map.get(pointer.table)
698        .and_then(JsonValue::as_object)
699        .and_then(|definitions| definitions.get(&pointer.name))
700}
701
702fn parse_local_definition_ref(schema_ref: &str) -> Option<DefinitionPointer> {
703    let fragment = schema_ref.strip_prefix('#')?;
704    let pointer = urlencoding::decode(fragment).ok()?;
705    let pointer = jsonptr::Pointer::parse(pointer.as_ref()).ok()?;
706
707    let (table_token, pointer) = pointer.split_front()?;
708    let table = table_token.decoded();
709    let table = DEFINITION_TABLE_KEYS
710        .into_iter()
711        .find(|candidate| table.as_ref() == *candidate)?;
712
713    // Responses API non-strict mode accepts nested local refs such as
714    // `#/$defs/User/properties/name`, so keep the parent definition reachable.
715    let (name, _) = pointer.split_front()?;
716    Some(DefinitionPointer {
717        table,
718        name: name.decoded().into_owned(),
719    })
720}
721
722fn normalized_schema_types(
723    map: &serde_json::Map<String, JsonValue>,
724) -> Vec<JsonSchemaPrimitiveType> {
725    let Some(schema_type) = map.get("type") else {
726        return Vec::new();
727    };
728
729    match schema_type {
730        JsonValue::String(schema_type) => schema_type_from_str(schema_type).into_iter().collect(),
731        JsonValue::Array(schema_types) => schema_types
732            .iter()
733            .filter_map(JsonValue::as_str)
734            .filter_map(schema_type_from_str)
735            .collect(),
736        _ => Vec::new(),
737    }
738}
739
740fn write_schema_types(
741    map: &mut serde_json::Map<String, JsonValue>,
742    schema_types: &[JsonSchemaPrimitiveType],
743) {
744    match schema_types {
745        [] => {
746            map.remove("type");
747        }
748        [schema_type] => {
749            map.insert(
750                "type".to_string(),
751                JsonValue::String(schema_type_name(*schema_type).to_string()),
752            );
753        }
754        _ => {
755            map.insert(
756                "type".to_string(),
757                JsonValue::Array(
758                    schema_types
759                        .iter()
760                        .map(|schema_type| {
761                            JsonValue::String(schema_type_name(*schema_type).to_string())
762                        })
763                        .collect(),
764                ),
765            );
766        }
767    }
768}
769
770fn schema_type_from_str(schema_type: &str) -> Option<JsonSchemaPrimitiveType> {
771    match schema_type {
772        "string" => Some(JsonSchemaPrimitiveType::String),
773        "number" => Some(JsonSchemaPrimitiveType::Number),
774        "boolean" => Some(JsonSchemaPrimitiveType::Boolean),
775        "integer" => Some(JsonSchemaPrimitiveType::Integer),
776        "object" => Some(JsonSchemaPrimitiveType::Object),
777        "array" => Some(JsonSchemaPrimitiveType::Array),
778        "null" => Some(JsonSchemaPrimitiveType::Null),
779        _ => None,
780    }
781}
782
783fn schema_type_name(schema_type: JsonSchemaPrimitiveType) -> &'static str {
784    match schema_type {
785        JsonSchemaPrimitiveType::String => "string",
786        JsonSchemaPrimitiveType::Number => "number",
787        JsonSchemaPrimitiveType::Boolean => "boolean",
788        JsonSchemaPrimitiveType::Integer => "integer",
789        JsonSchemaPrimitiveType::Object => "object",
790        JsonSchemaPrimitiveType::Array => "array",
791        JsonSchemaPrimitiveType::Null => "null",
792    }
793}
794
795fn singleton_null_schema_error() -> serde_json::Error {
796    serde_json::Error::io(std::io::Error::new(
797        std::io::ErrorKind::InvalidInput,
798        "tool input schema must not be a singleton null type",
799    ))
800}
801
802#[cfg(test)]
803#[path = "json_schema_tests.rs"]
804mod tests;