zeroclaw-labs/zeroclaw · error · anyhow::Error

Schema missing required 'type' field

Error message

Schema missing required 'type' field

What it means

The Anthropic provider serializes its internally-constructed NativeChatRequest with serde_json::to_value before entering the async stream, so the request body is owned and 'static across the await boundary. to_value on this plain derived struct (strings, Options, Vecs, numbers) is infallible in practice; the expect marks that invariant. A failure would mean a field type whose Serialize impl errors — for example a map with non-string keys or a custom serializer with error paths added later — not anything about the request content or credentials.

Source

Thrown at crates/zeroclaw-api/src/schema.rs:206

            Self::extract_defs(obj)
        } else {
            HashMap::new()
        };

        Self::clean_with_defs(schema, &defs, strategy, &mut HashSet::new())
    }

    /// Validate that a schema is suitable for LLM tool calling.
    ///
    /// Returns an error if the schema is invalid or missing required fields.
    pub fn validate(schema: &Value) -> anyhow::Result<()> {
        let obj = schema
            .as_object()
            .ok_or_else(|| anyhow::Error::msg("Schema must be an object"))?;

        // Must have 'type' field
        if !obj.contains_key("type") {
            anyhow::bail!("Schema missing required 'type' field");
        }

        // If type is 'object', should have 'properties'
        if let Some(Value::String(t)) = obj.get("type")
            && t == "object"
            && !obj.contains_key("properties")
        {
            eprintln!("warn: Object schema without 'properties' field may cause issues");
        }

        Ok(())
    }

    // --------------------------------------------------------------------
    // Internal implementation
    // --------------------------------------------------------------------

    /// Extract $defs and definitions into a flat map for reference resolution.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. If hit after modifying the request struct, bisect the recently added fields and check their Serialize impls.
  2. Add a unit test that builds a representative NativeChatRequest for every code path and asserts serde_json::to_value succeeds.
  3. As a maintainer, replace the expect with map_err into the provider's error stream so a serialization bug degrades to a per-request error instead of a panic.

Example fix

// before
let body = serde_json::to_value(&native_request)
    .expect("NativeChatRequest should serialize to JSON");

// after
let body = match serde_json::to_value(&native_request) {
    Ok(v) => v,
    Err(e) => {
        return stream::once(async move {
            Err(provider_error(format!("failed to serialize chat request: {e}")))
        })
    }
};
Defensive patterns

Strategy: validation

Validate before calling

// Pin the invariant the expect asserts, in your test suite:
#[test]
fn native_request_serializes_for_all_paths() {
    for req in representative_native_requests() {
        assert!(serde_json::to_value(&req).is_ok(),
            "NativeChatRequest failed to serialize: {req:?}");
    }
}

Prevention

When it happens

Trigger: Not reachable through configuration or user input with the current struct shape; it would surface only after a code change adds a field whose Serialize can fail, or a manual Serialize impl with error paths on NativeChatRequest.

Common situations: Extending NativeChatRequest with an exotic key type (non-string map keys), an untagged enum edge case, or a custom serializer; virtually never seen from configuration or network conditions.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3ca041f89f886bcb. Report an issue: GitHub.