1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ from google .adk .tools ._gemini_schema_util import _sanitize_schema_formats_for_gemini
1516from google .adk .tools ._gemini_schema_util import _to_gemini_schema
1617from google .adk .tools ._gemini_schema_util import _to_snake_case
1718from google .genai .types import Schema
@@ -31,7 +32,7 @@ def test_to_gemini_schema_not_dict(self):
3132 def test_to_gemini_schema_empty_dict (self ):
3233 result = _to_gemini_schema ({})
3334 assert isinstance (result , Schema )
34- assert result .type is None
35+ assert result .type is Type . OBJECT
3536 assert result .properties is None
3637
3738 def test_to_gemini_schema_dict_with_only_object_type (self ):
@@ -64,10 +65,8 @@ def test_to_gemini_schema_array_string_types(self):
6465 "nonnullable_string" : {"type" : ["string" ]},
6566 "nullable_string" : {"type" : ["string" , "null" ]},
6667 "nullable_number" : {"type" : ["null" , "integer" ]},
67- "object_nullable" : {"type" : "null" }, # invalid
68- "multi_types_nullable" : {
69- "type" : ["string" , "null" , "integer" ]
70- }, # invalid
68+ "object_nullable" : {"type" : "null" },
69+ "multi_types_nullable" : {"type" : ["string" , "null" , "integer" ]},
7170 "empty_default_object" : {},
7271 },
7372 }
@@ -85,14 +84,14 @@ def test_to_gemini_schema_array_string_types(self):
8584 assert gemini_schema .properties ["nullable_number" ].type == Type .INTEGER
8685 assert gemini_schema .properties ["nullable_number" ].nullable
8786
88- assert gemini_schema .properties ["object_nullable" ].type is None
87+ assert gemini_schema .properties ["object_nullable" ].type == Type . OBJECT
8988 assert gemini_schema .properties ["object_nullable" ].nullable
9089
91- assert gemini_schema .properties ["multi_types_nullable" ].type is None
90+ assert gemini_schema .properties ["multi_types_nullable" ].type == Type . STRING
9291 assert gemini_schema .properties ["multi_types_nullable" ].nullable
9392
94- assert gemini_schema .properties ["empty_default_object" ].type is None
95- assert not gemini_schema .properties ["empty_default_object" ].nullable
93+ assert gemini_schema .properties ["empty_default_object" ].type == Type . OBJECT
94+ assert gemini_schema .properties ["empty_default_object" ].nullable is None
9695
9796 def test_to_gemini_schema_nested_objects (self ):
9897 openapi_schema = {
@@ -382,6 +381,136 @@ def test_to_gemini_schema_property_ordering(self):
382381 gemini_schema = _to_gemini_schema (openapi_schema )
383382 assert gemini_schema .property_ordering == ["name" , "age" ]
384383
384+ def test_sanitize_schema_formats_for_gemini (self ):
385+ schema = {
386+ "type" : "object" ,
387+ "description" : "Test schema" , # Top-level description
388+ "properties" : {
389+ "valid_int" : {"type" : "integer" , "format" : "int32" },
390+ "invalid_format_prop" : {"type" : "integer" , "format" : "unsigned" },
391+ "valid_string" : {"type" : "string" , "format" : "date-time" },
392+ "camelCaseKey" : {"type" : "string" },
393+ "prop_with_extra_key" : {
394+ "type" : "boolean" ,
395+ "unknownInternalKey" : "discard_this_value" ,
396+ },
397+ },
398+ "required" : ["valid_int" ],
399+ "additionalProperties" : False , # This is an unsupported top-level key
400+ "unknownTopLevelKey" : (
401+ "discard_me_too"
402+ ), # Another unsupported top-level key
403+ }
404+ sanitized = _sanitize_schema_formats_for_gemini (schema )
405+
406+ # Check description is preserved
407+ assert sanitized ["description" ] == "Test schema"
408+
409+ # Check properties and their sanitization
410+ assert "properties" in sanitized
411+ sanitized_props = sanitized ["properties" ]
412+
413+ assert "valid_int" in sanitized_props
414+ assert sanitized_props ["valid_int" ]["type" ] == "integer"
415+ assert sanitized_props ["valid_int" ]["format" ] == "int32"
416+
417+ assert "invalid_format_prop" in sanitized_props
418+ assert sanitized_props ["invalid_format_prop" ]["type" ] == "integer"
419+ assert (
420+ "format" not in sanitized_props ["invalid_format_prop" ]
421+ ) # Invalid format removed
422+
423+ assert "valid_string" in sanitized_props
424+ assert sanitized_props ["valid_string" ]["type" ] == "string"
425+ assert sanitized_props ["valid_string" ]["format" ] == "date-time"
426+
427+ # Check camelCase keys not changed for properties
428+ assert "camel_case_key" not in sanitized_props
429+ assert "camelCaseKey" in sanitized_props
430+ assert sanitized_props ["camelCaseKey" ]["type" ] == "string"
431+
432+ # Check removal of unsupported keys within a property definition
433+ assert "prop_with_extra_key" in sanitized_props
434+ assert sanitized_props ["prop_with_extra_key" ]["type" ] == "boolean"
435+ assert (
436+ "unknown_internal_key" # snake_cased version of unknownInternalKey
437+ not in sanitized_props ["prop_with_extra_key" ]
438+ )
439+
440+ # Check removal of unsupported top-level fields (after snake_casing)
441+ assert "additional_properties" not in sanitized
442+ assert "unknown_top_level_key" not in sanitized
443+
444+ # Check original unsupported top-level field names are not there either
445+ assert "additionalProperties" not in sanitized
446+ assert "unknownTopLevelKey" not in sanitized
447+
448+ # Check required is preserved
449+ assert sanitized ["required" ] == ["valid_int" ]
450+
451+ # Test with a schema that has a list of types for a property
452+ schema_with_list_type = {
453+ "type" : "object" ,
454+ "properties" : {
455+ "nullable_field" : {"type" : ["string" , "null" ], "format" : "uuid" }
456+ },
457+ }
458+ sanitized_list_type = _sanitize_schema_formats_for_gemini (
459+ schema_with_list_type
460+ )
461+ # format should be removed because 'uuid' is not supported for string
462+ assert "format" not in sanitized_list_type ["properties" ]["nullable_field" ]
463+ # type should be processed by _sanitize_schema_type and preserved
464+ assert sanitized_list_type ["properties" ]["nullable_field" ]["type" ] == [
465+ "string" ,
466+ "null" ,
467+ ]
468+
469+ def test_sanitize_schema_formats_for_gemini_nullable (self ):
470+ openapi_schema = {
471+ "properties" : {
472+ "case_id" : {
473+ "description" : "The ID of the case." ,
474+ "title" : "Case Id" ,
475+ "type" : "string" ,
476+ },
477+ "next_page_token" : {
478+ "anyOf" : [{"type" : "string" }, {"type" : "null" }],
479+ "default" : None ,
480+ "description" : (
481+ "The nextPageToken to fetch the next page of results."
482+ ),
483+ "title" : "Next Page Token" ,
484+ },
485+ },
486+ "required" : ["case_id" ],
487+ "title" : "list_alerts_by_caseArguments" ,
488+ "type" : "object" ,
489+ }
490+ openapi_schema = _sanitize_schema_formats_for_gemini (openapi_schema )
491+ assert openapi_schema == {
492+ "properties" : {
493+ "case_id" : {
494+ "description" : "The ID of the case." ,
495+ "title" : "Case Id" ,
496+ "type" : "string" ,
497+ },
498+ "next_page_token" : {
499+ "any_of" : [
500+ {"type" : "string" },
501+ {"type" : ["object" , "null" ]},
502+ ],
503+ "description" : (
504+ "The nextPageToken to fetch the next page of results."
505+ ),
506+ "title" : "Next Page Token" ,
507+ },
508+ },
509+ "required" : ["case_id" ],
510+ "title" : "list_alerts_by_caseArguments" ,
511+ "type" : "object" ,
512+ }
513+
385514
386515class TestToSnakeCase :
387516
0 commit comments