use adk_core::SchemaAdapter;
use adk_core::schema_utils;
use serde_json::{Map, Value};
use std::borrow::Cow;
const GEMINI_ALLOWED_FORMATS: &[&str] =
&["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];
const UNSUPPORTED_KEYWORDS: &[&str] = &[
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"not",
"propertyNames",
"patternProperties",
"unevaluatedProperties",
];
const UNSUPPORTED_KEYWORDS_VERTEX: &[&str] = &[
"exclusiveMinimum",
"exclusiveMaximum",
"not",
"propertyNames",
"patternProperties",
"unevaluatedProperties",
];
#[derive(Debug)]
pub struct GeminiSchemaAdapter {
vertex_ai: bool,
}
impl GeminiSchemaAdapter {
pub fn new() -> Self {
Self { vertex_ai: false }
}
pub fn vertex_ai() -> Self {
Self { vertex_ai: true }
}
}
impl Default for GeminiSchemaAdapter {
fn default() -> Self {
Self::new()
}
}
impl SchemaAdapter for GeminiSchemaAdapter {
fn normalize_schema(&self, mut schema: Value) -> Value {
let definitions = extract_definitions(&schema);
schema_utils::resolve_refs(&mut schema, &definitions, 0);
schema_utils::strip_schema_keyword(&mut schema);
schema_utils::collapse_combiners(&mut schema);
schema_utils::merge_all_of(&mut schema);
schema_utils::collapse_type_arrays(&mut schema);
schema_utils::strip_conditional_keywords(&mut schema);
schema_utils::convert_const_to_enum(&mut schema);
schema_utils::strip_null_from_enum(&mut schema);
schema_utils::add_implicit_object_type(&mut schema);
if self.vertex_ai {
remove_unsupported_keywords_vertex(&mut schema);
} else {
remove_unsupported_keywords(&mut schema);
}
schema_utils::strip_unsupported_formats(&mut schema, GEMINI_ALLOWED_FORMATS);
schema_utils::enforce_nesting_depth(&mut schema, 5, 0);
if let Some(obj) = schema.as_object_mut() {
obj.remove("definitions");
obj.remove("$defs");
}
schema
}
fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
if name.len() <= 64 {
Cow::Borrowed(name)
} else {
let mut end = 64;
while end > 0 && !name.is_char_boundary(end) {
end -= 1;
}
Cow::Owned(name[..end].to_string())
}
}
fn empty_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
}
fn extract_definitions(schema: &Value) -> Map<String, Value> {
let mut defs = Map::new();
if let Some(obj) = schema.as_object() {
if let Some(definitions) = obj.get("definitions").and_then(|v| v.as_object()) {
for (key, value) in definitions {
defs.insert(key.clone(), value.clone());
}
}
if let Some(dollar_defs) = obj.get("$defs").and_then(|v| v.as_object()) {
for (key, value) in dollar_defs {
defs.insert(key.clone(), value.clone());
}
}
}
defs
}
fn remove_unsupported_keywords(schema: &mut Value) {
let Some(obj) = schema.as_object_mut() else {
return;
};
for keyword in UNSUPPORTED_KEYWORDS {
obj.remove(*keyword);
}
let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
if !is_array_type {
obj.remove("items");
}
if let Some(props) = obj.get_mut("properties")
&& let Some(props_obj) = props.as_object_mut()
{
for value in props_obj.values_mut() {
remove_unsupported_keywords(value);
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
remove_unsupported_keywords(items);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
remove_unsupported_keywords(item);
}
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword)
&& let Some(arr) = arr_val.as_array_mut()
{
for sub in arr.iter_mut() {
remove_unsupported_keywords(sub);
}
}
}
if let Some(prefix_items) = obj.get_mut("prefixItems")
&& let Some(arr) = prefix_items.as_array_mut()
{
for item in arr.iter_mut() {
remove_unsupported_keywords(item);
}
}
}
fn remove_unsupported_keywords_vertex(schema: &mut Value) {
let Some(obj) = schema.as_object_mut() else {
return;
};
for keyword in UNSUPPORTED_KEYWORDS_VERTEX {
obj.remove(*keyword);
}
let is_object_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object");
if is_object_type {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
} else {
obj.remove("additionalProperties");
}
let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
if !is_array_type {
obj.remove("items");
}
if let Some(props) = obj.get_mut("properties")
&& let Some(props_obj) = props.as_object_mut()
{
for value in props_obj.values_mut() {
remove_unsupported_keywords_vertex(value);
}
}
if let Some(items) = obj.get_mut("items") {
if items.is_object() {
remove_unsupported_keywords_vertex(items);
} else if let Some(arr) = items.as_array_mut() {
for item in arr.iter_mut() {
remove_unsupported_keywords_vertex(item);
}
}
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(arr_val) = obj.get_mut(*keyword)
&& let Some(arr) = arr_val.as_array_mut()
{
for sub in arr.iter_mut() {
remove_unsupported_keywords_vertex(sub);
}
}
}
if let Some(prefix_items) = obj.get_mut("prefixItems")
&& let Some(arr) = prefix_items.as_array_mut()
{
for item in arr.iter_mut() {
remove_unsupported_keywords_vertex(item);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_strips_schema_keyword() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": { "name": { "type": "string" } }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("$schema").is_none());
}
#[test]
fn test_removes_additional_properties() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"additionalProperties": true
});
let result = adapter.normalize_schema(schema);
assert!(result.get("additionalProperties").is_none());
}
#[test]
fn test_removes_exclusive_min_max() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100
});
let result = adapter.normalize_schema(schema);
assert!(result.get("exclusiveMinimum").is_none());
assert!(result.get("exclusiveMaximum").is_none());
}
#[test]
fn test_removes_items_when_not_array() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"items": { "type": "string" }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("items").is_none());
}
#[test]
fn test_preserves_items_when_array() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "array",
"items": { "type": "string" }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("items").is_some());
assert_eq!(result["items"]["type"], "string");
}
#[test]
fn test_removes_not_keyword() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "string",
"not": { "enum": ["bad"] }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("not").is_none());
}
#[test]
fn test_removes_property_names() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"propertyNames": { "pattern": "^[a-z]+$" }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("propertyNames").is_none());
}
#[test]
fn test_removes_pattern_properties() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"patternProperties": { "^S_": { "type": "string" } }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("patternProperties").is_none());
}
#[test]
fn test_removes_unevaluated_properties() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"unevaluatedProperties": false
});
let result = adapter.normalize_schema(schema);
assert!(result.get("unevaluatedProperties").is_none());
}
#[test]
fn test_collapses_any_of() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"anyOf": [
{ "type": "null" },
{ "type": "string", "minLength": 1 }
]
});
let result = adapter.normalize_schema(schema);
assert!(result.get("anyOf").is_none());
assert_eq!(result["type"], "string");
assert_eq!(result["minLength"], 1);
}
#[test]
fn test_collapses_one_of() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"oneOf": [
{ "type": "null" },
{ "type": "integer", "minimum": 0 }
]
});
let result = adapter.normalize_schema(schema);
assert!(result.get("oneOf").is_none());
assert_eq!(result["type"], "integer");
}
#[test]
fn test_merges_all_of() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"allOf": [
{ "type": "object", "properties": { "a": { "type": "string" } } },
{ "properties": { "b": { "type": "number" } }, "required": ["b"] }
]
});
let result = adapter.normalize_schema(schema);
assert!(result.get("allOf").is_none());
assert_eq!(result["properties"]["a"]["type"], "string");
assert_eq!(result["properties"]["b"]["type"], "number");
assert_eq!(result["required"], json!(["b"]));
}
#[test]
fn test_collapses_type_arrays() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": ["string", "null"],
"minLength": 1
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["type"], "string");
}
#[test]
fn test_strips_conditional_keywords() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"if": { "properties": { "kind": { "const": "a" } } },
"then": { "required": ["extra"] },
"else": { "required": [] }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("if").is_none());
assert!(result.get("then").is_none());
assert!(result.get("else").is_none());
}
#[test]
fn test_converts_const_to_enum() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "string",
"const": "fixed"
});
let result = adapter.normalize_schema(schema);
assert!(result.get("const").is_none());
assert_eq!(result["enum"], json!(["fixed"]));
}
#[test]
fn test_strips_null_from_enum() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "string",
"enum": ["a", null, "b"]
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["enum"], json!(["a", "b"]));
}
#[test]
fn test_removes_empty_enum_after_null_strip() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "string",
"enum": [null]
});
let result = adapter.normalize_schema(schema);
assert!(result.get("enum").is_none());
}
#[test]
fn test_adds_implicit_object_type() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"properties": { "name": { "type": "string" } }
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["type"], "object");
}
#[test]
fn test_strips_unsupported_formats() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"created": { "type": "string", "format": "date-time" },
"hostname": { "type": "string", "format": "hostname" },
"id": { "type": "string", "format": "uuid" }
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["properties"]["created"]["format"], "date-time");
assert!(result["properties"]["hostname"].get("format").is_none());
assert_eq!(result["properties"]["id"]["format"], "uuid");
}
#[test]
fn test_preserves_all_allowed_formats() {
let adapter = GeminiSchemaAdapter::new();
for format in GEMINI_ALLOWED_FORMATS {
let schema = json!({ "type": "string", "format": format });
let result = adapter.normalize_schema(schema);
assert_eq!(result["format"], *format, "format '{format}' should be preserved");
}
}
#[test]
fn test_enforces_nesting_depth() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"l1": {
"type": "object",
"properties": {
"l2": {
"type": "object",
"properties": {
"l3": {
"type": "object",
"properties": {
"l4": {
"type": "object",
"properties": {
"l5": {
"type": "object",
"properties": {
"l6": { "type": "string" }
}
}
}
}
}
}
}
}
}
}
}
});
let result = adapter.normalize_schema(schema);
let l5 = &result["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]
["l4"]["properties"]["l5"];
assert_eq!(l5, &json!({"type": "object"}));
}
#[test]
fn test_resolves_refs() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"address": { "$ref": "#/definitions/Address" }
},
"definitions": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" }
}
}
}
});
let result = adapter.normalize_schema(schema);
assert!(result["properties"]["address"].get("$ref").is_none());
assert_eq!(result["properties"]["address"]["type"], "object");
assert_eq!(result["properties"]["address"]["properties"]["street"]["type"], "string");
assert!(result.get("definitions").is_none());
}
#[test]
fn test_resolves_dollar_defs() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"item": { "$ref": "#/$defs/Item" }
},
"$defs": {
"Item": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
}
});
let result = adapter.normalize_schema(schema);
assert!(result["properties"]["item"].get("$ref").is_none());
assert_eq!(result["properties"]["item"]["type"], "object");
assert!(result.get("$defs").is_none());
}
#[test]
fn test_unresolvable_ref_becomes_object() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"unknown": { "$ref": "#/definitions/DoesNotExist" }
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["properties"]["unknown"], json!({"type": "object"}));
}
#[test]
fn test_circular_ref_breaks() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"self_ref": { "$ref": "#/definitions/Node" }
},
"definitions": {
"Node": {
"type": "object",
"properties": {
"child": { "$ref": "#/definitions/Node" }
}
}
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["properties"]["self_ref"]["type"], "object");
assert!(result.get("definitions").is_none());
}
#[test]
fn test_removes_definitions_and_defs() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"definitions": { "Foo": { "type": "string" } },
"$defs": { "Bar": { "type": "number" } }
});
let result = adapter.normalize_schema(schema);
assert!(result.get("definitions").is_none());
assert!(result.get("$defs").is_none());
}
#[test]
fn test_nested_unsupported_keywords_removed() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": {
"inner": {
"type": "object",
"additionalProperties": false,
"exclusiveMinimum": 5,
"properties": {
"deep": {
"type": "number",
"exclusiveMaximum": 100
}
}
}
}
});
let result = adapter.normalize_schema(schema);
let inner = &result["properties"]["inner"];
assert!(inner.get("additionalProperties").is_none());
assert!(inner.get("exclusiveMinimum").is_none());
assert!(inner["properties"]["deep"].get("exclusiveMaximum").is_none());
}
#[test]
fn test_full_transform_pipeline() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Status": { "type": "string", "enum": ["active", null, "inactive"] }
},
"properties": {
"name": { "type": ["string", "null"], "format": "hostname" },
"status": { "$ref": "#/definitions/Status" },
"config": {
"type": "object",
"additionalProperties": true,
"properties": {
"value": { "const": "fixed" }
}
}
},
"if": { "properties": { "name": { "type": "string" } } },
"then": { "required": ["status"] },
"additionalProperties": false
});
let result = adapter.normalize_schema(schema);
assert!(result.get("$schema").is_none());
assert!(result.get("definitions").is_none());
assert!(result.get("if").is_none());
assert!(result.get("then").is_none());
assert!(result.get("additionalProperties").is_none());
assert_eq!(result["properties"]["name"]["type"], "string");
assert!(result["properties"]["name"].get("format").is_none());
assert_eq!(result["properties"]["status"]["enum"], json!(["active", "inactive"]));
assert_eq!(result["properties"]["config"]["properties"]["value"]["enum"], json!(["fixed"]));
assert!(result["properties"]["config"].get("additionalProperties").is_none());
assert_eq!(result["type"], "object");
}
#[test]
fn test_idempotent() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "type": ["string", "null"], "format": "hostname" },
"items": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true,
"if": { "const": true },
"then": { "required": ["name"] }
});
let first = adapter.normalize_schema(schema);
let second = adapter.normalize_schema(first.clone());
assert_eq!(first, second);
}
#[test]
fn test_empty_schema() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({});
let result = adapter.normalize_schema(schema);
assert_eq!(result, json!({}));
}
#[test]
fn test_array_items_nested_cleanup() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"id": { "type": "integer", "exclusiveMinimum": 0 }
}
}
});
let result = adapter.normalize_schema(schema);
assert!(result["items"].get("additionalProperties").is_none());
assert!(result["items"]["properties"]["id"].get("exclusiveMinimum").is_none());
}
#[test]
fn test_vertex_ai_sets_additional_properties_false() {
let adapter = GeminiSchemaAdapter::vertex_ai();
let schema = json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"additionalProperties": true
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn test_vertex_ai_sets_additional_properties_false_on_nested_objects() {
let adapter = GeminiSchemaAdapter::vertex_ai();
let schema = json!({
"type": "object",
"properties": {
"inner": {
"type": "object",
"properties": {
"value": { "type": "string" }
}
}
}
});
let result = adapter.normalize_schema(schema);
assert_eq!(result["additionalProperties"], json!(false));
assert_eq!(result["properties"]["inner"]["additionalProperties"], json!(false));
}
#[test]
fn test_vertex_ai_does_not_set_additional_properties_on_non_object() {
let adapter = GeminiSchemaAdapter::vertex_ai();
let schema = json!({
"type": "string",
"additionalProperties": true
});
let result = adapter.normalize_schema(schema);
assert!(result.get("additionalProperties").is_none());
}
#[test]
fn test_standard_mode_removes_additional_properties() {
let adapter = GeminiSchemaAdapter::new();
let schema = json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"additionalProperties": true
});
let result = adapter.normalize_schema(schema);
assert!(result.get("additionalProperties").is_none());
}
#[test]
fn test_vertex_ai_still_removes_other_unsupported_keywords() {
let adapter = GeminiSchemaAdapter::vertex_ai();
let schema = json!({
"type": "object",
"properties": { "x": { "type": "number" } },
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
"not": { "type": "null" },
"propertyNames": { "pattern": "^[a-z]" },
"patternProperties": { "^S_": { "type": "string" } },
"unevaluatedProperties": false
});
let result = adapter.normalize_schema(schema);
assert!(result.get("exclusiveMinimum").is_none());
assert!(result.get("exclusiveMaximum").is_none());
assert!(result.get("not").is_none());
assert!(result.get("propertyNames").is_none());
assert!(result.get("patternProperties").is_none());
assert!(result.get("unevaluatedProperties").is_none());
assert_eq!(result["additionalProperties"], json!(false));
}
#[test]
fn test_normalize_tool_name_short_name_unchanged() {
let adapter = GeminiSchemaAdapter::new();
let name = "get_weather";
let result = adapter.normalize_tool_name(name);
assert_eq!(result, "get_weather");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_normalize_tool_name_exactly_64_bytes() {
let adapter = GeminiSchemaAdapter::new();
let name = "a".repeat(64);
let result = adapter.normalize_tool_name(&name);
assert_eq!(result.len(), 64);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_normalize_tool_name_truncates_at_64_bytes() {
let adapter = GeminiSchemaAdapter::new();
let name = "a".repeat(100);
let result = adapter.normalize_tool_name(&name);
assert_eq!(result.len(), 64);
assert_eq!(result.as_ref(), "a".repeat(64));
}
#[test]
fn test_normalize_tool_name_multibyte_boundary() {
let adapter = GeminiSchemaAdapter::new();
let name = "日".repeat(22); let result = adapter.normalize_tool_name(&name);
assert!(result.len() <= 64);
assert_eq!(result.len(), 63);
assert_eq!(result.as_ref(), "日".repeat(21));
assert!(std::str::from_utf8(result.as_bytes()).is_ok());
}
#[test]
fn test_normalize_tool_name_emoji_boundary() {
let adapter = GeminiSchemaAdapter::new();
let name = "🎯".repeat(16);
assert_eq!(name.len(), 64);
let result = adapter.normalize_tool_name(&name);
assert_eq!(result.len(), 64);
let name = "🎯".repeat(17);
let result = adapter.normalize_tool_name(&name);
assert_eq!(result.len(), 64);
assert_eq!(result.as_ref(), "🎯".repeat(16));
}
#[test]
fn test_empty_schema_returns_object_with_properties() {
let adapter = GeminiSchemaAdapter::new();
let result = adapter.empty_schema();
assert_eq!(result, json!({"type": "object", "properties": {}}));
}
#[test]
fn test_empty_schema_vertex_ai_same_as_standard() {
let adapter = GeminiSchemaAdapter::vertex_ai();
let result = adapter.empty_schema();
assert_eq!(result, json!({"type": "object", "properties": {}}));
}
}