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

  1. 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
  2. Pin schemars to the version codex-rs was built against if the panic appeared after a dependency refresh
  3. Run cargo clean and rebuild to rule out stale artifacts
  4. 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

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


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/fb2ad1777a621b03. Report an issue: GitHub.