openai/codex · critical
root tool schema must be an object
Error message
root tool schema must be an object
What it means
This unreachable! in schema_for (codex-rs/ext/skills/src/tools/schema.rs:24-26) guards the destructure of the serialized root schema into a JSON object. schemars root schemas are always objects (a map of $schema/title/type/properties/...), so the branch is considered impossible. It would fire only if the JsonSchema implementation for the tool type - or a future schemars major version - emitted a non-object root, for example a boolean schema (true/false) for an unconstrained or never-valid type, or an array.
Source
Thrown at codex-rs/ext/skills/src/tools/schema.rs:25
schema_for::<T>(/*option_add_null_type*/ false)
}
pub(super) fn output_schema_for<T: JsonSchema>() -> Value {
schema_for::<T>(/*option_add_null_type*/ true)
}
fn schema_for<T: JsonSchema>(option_add_null_type: bool) -> Value {
let schema = SchemaSettings::draft2019_09()
.with(|settings| {
settings.inline_subschemas = true;
settings.option_add_null_type = option_add_null_type;
})
.into_generator()
.into_root_schema_for::<T>();
let schema_value = serde_json::to_value(schema)
.unwrap_or_else(|err| panic!("generated skill tool schema should serialize: {err}"));
let Value::Object(mut schema_object) = schema_value else {
unreachable!("root tool schema must be an object");
};
let mut tool_schema = Map::new();
for key in [
"properties",
"required",
"type",
"additionalProperties",
"$defs",
"definitions",
] {
if let Some(value) = schema_object.remove(key) {
tool_schema.insert(key.to_string(), value);
}
}
Value::Object(tool_schema)
}
View on GitHub (pinned to 339751715c)
Solutions
- Inspect the offending tool type: make it a plain struct (or enum with struct variants) deriving JsonSchema, not a type backed by Schema::Bool or a custom impl
- Pin schemars to the version codex-rs was built against if the panic appeared after a dependency refresh
- Run cargo clean and rebuild to rule out stale artifacts
- If reproducible on a clean build with stock deps, report upstream to codex-rs with the type definition
Example fix
// before
#[derive(JsonSchema)]
struct MySchemaWrapper(schemars::schema::Schema); // root may serialize to true/false
// after
#[derive(Deserialize, JsonSchema)]
struct MyArgs {
query: String,
} Defensive patterns
Strategy: try-catch
Validate before calling
// inside the skills crate: assert an object root per tool before shipping
#[test]
fn tool_schema_has_object_root() {
assert!(matches!(
schema::input_schema_for::<MyArgs>(),
serde_json::Value::Object(_)
));
} Type guard
fn tool_args_have_object_root<I: schemars::JsonSchema>() -> bool {
matches!(
serde_json::to_value(schemars::schema_for!(I)).ok(),
Some(serde_json::Value::Object(_))
)
} Try / catch
let built = std::panic::catch_unwind(|| {
skill_function_tool::<MyArgs, MyOut>("my_tool", "desc")
});
if built.is_err() {
tracing::error!("skill schema generation panicked; skipping tool");
} Prevention
- Derive JsonSchema on plain structs; avoid custom JsonSchema impls and Schema-typed wrappers for tool args
- Lock schemars to the version codex-rs ships with
- A Value::Object root assertion per tool turns this unreachable into a build-time failure
When it happens
Trigger: A tool input type whose derived root schema serializes to true/false/array instead of an object - possible with custom JsonSchema impls, Schema-typed wrappers, or a schemars upgrade that starts emitting boolean root schemas. Fires during skill tool registration.
Common situations: Upgrading schemars across a major version that changes root-schema emission; a hand-written JsonSchema impl returning Schema::Bool(true); practically unseen on stock codex-rs builds.
Related errors
- generated input schema for {name} should parse: {err}
- generated skill tool schema should serialize: {err}
- invalid bundled bubblewrap fd path: {err}
- plugin cache root should be absolute: {err}
- mutex poisoned
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/fb2ad1777a621b03.
Report an issue: GitHub.