# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from google.adk.tools._gemini_schema_util import _sanitize_schema_formats_for_gemini from google.adk.tools._gemini_schema_util import _to_gemini_schema from google.adk.tools._gemini_schema_util import _to_snake_case from google.genai.types import Schema from google.genai.types import Type import pytest class TestToGeminiSchema: def test_to_gemini_schema_none(self): assert _to_gemini_schema(None) is None def test_to_gemini_schema_not_dict(self): with pytest.raises(TypeError, match="openapi_schema must be a dictionary"): _to_gemini_schema("not a dict") def test_to_gemini_schema_empty_dict(self): result = _to_gemini_schema({}) assert isinstance(result, Schema) assert result.type is Type.OBJECT assert result.properties is None def test_to_gemini_schema_dict_with_only_object_type(self): result = _to_gemini_schema({"type": "object"}) assert isinstance(result, Schema) assert result.type == Type.OBJECT assert result.properties is None def test_to_gemini_schema_basic_types(self): openapi_schema = { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, "is_active": {"type": "boolean"}, }, } gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["name"].type == Type.STRING assert gemini_schema.properties["age"].type == Type.INTEGER assert gemini_schema.properties["is_active"].type == Type.BOOLEAN def test_to_gemini_schema_array_string_types(self): openapi_schema = { "type": "object", "properties": { "boolean_field": {"type": "boolean"}, "nonnullable_string": {"type": ["string"]}, "nullable_string": {"type": ["string", "null"]}, "nullable_number": {"type": ["null", "integer"]}, "nullable_object": {"type": ["object", "null"]}, "object_nullable": {"type": "null"}, "multi_types_nullable": {"type": ["string", "null", "integer"]}, "only_null": {"type": "null"}, "empty_default_object": {}, "empty_list_type": {"type": []}, "multi_type_with_array_nullable": { "type": ["string", "array", "null"] }, "multi_type_with_array_nonnullable": {"type": ["integer", "array"]}, }, } gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["boolean_field"].type == Type.BOOLEAN assert gemini_schema.properties["nonnullable_string"].type == Type.STRING assert not gemini_schema.properties["nonnullable_string"].nullable assert gemini_schema.properties["nullable_string"].type == Type.STRING assert gemini_schema.properties["nullable_string"].nullable assert gemini_schema.properties["nullable_number"].type == Type.INTEGER assert gemini_schema.properties["nullable_number"].nullable assert gemini_schema.properties["nullable_object"].type == Type.OBJECT assert gemini_schema.properties["nullable_object"].nullable assert gemini_schema.properties["object_nullable"].type == Type.OBJECT assert gemini_schema.properties["object_nullable"].nullable assert gemini_schema.properties["multi_types_nullable"].type == Type.STRING assert gemini_schema.properties["multi_types_nullable"].nullable assert gemini_schema.properties["only_null"].type == Type.OBJECT assert gemini_schema.properties["only_null"].nullable assert gemini_schema.properties["multi_types_nullable"].type == Type.STRING assert gemini_schema.properties["multi_types_nullable"].nullable assert gemini_schema.properties["empty_default_object"].type == Type.OBJECT assert gemini_schema.properties["empty_default_object"].nullable is None assert gemini_schema.properties["empty_list_type"].type == Type.OBJECT assert not gemini_schema.properties["empty_list_type"].nullable assert ( gemini_schema.properties["multi_type_with_array_nullable"].type == Type.ARRAY ) assert gemini_schema.properties["multi_type_with_array_nullable"].nullable assert ( gemini_schema.properties["multi_type_with_array_nonnullable"].type == Type.ARRAY ) assert not gemini_schema.properties[ "multi_type_with_array_nonnullable" ].nullable def test_to_gemini_schema_nested_objects(self): openapi_schema = { "type": "object", "properties": { "address": { "type": "object", "properties": { "street": {"type": "string"}, "city": {"type": "string"}, }, } }, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.properties["address"].type == Type.OBJECT assert ( gemini_schema.properties["address"].properties["street"].type == Type.STRING ) assert ( gemini_schema.properties["address"].properties["city"].type == Type.STRING ) def test_to_gemini_schema_array(self): openapi_schema = { "type": "array", "items": {"type": "string"}, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.ARRAY assert gemini_schema.items.type == Type.STRING def test_to_gemini_schema_nested_array(self): openapi_schema = { "type": "array", "items": { "type": "object", "properties": {"name": {"type": "string"}}, }, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.items.properties["name"].type == Type.STRING def test_to_gemini_schema_array_without_items_gets_default(self): openapi_schema = {"type": "array"} gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.ARRAY assert not gemini_schema.nullable assert gemini_schema.items.type == Type.STRING def test_to_gemini_schema_nullable_array_without_items_gets_default(self): openapi_schema = {"type": ["array", "null"]} gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.ARRAY assert gemini_schema.nullable assert gemini_schema.items.type == Type.STRING def test_to_gemini_schema_any_of(self): openapi_schema = { "anyOf": [{"type": "string"}, {"type": "integer"}], } gemini_schema = _to_gemini_schema(openapi_schema) assert len(gemini_schema.any_of) == 2 assert gemini_schema.any_of[0].type == Type.STRING assert gemini_schema.any_of[1].type == Type.INTEGER def test_to_gemini_schema_any_of_nullable(self): openapi_schema = { "anyOf": [{"type": "string"}, {"type": "null"}], } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.STRING assert gemini_schema.nullable def test_to_gemini_schema_general_list(self): openapi_schema = { "type": "array", "properties": { "list_field": {"type": "array", "items": {"type": "string"}}, }, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.properties["list_field"].type == Type.ARRAY assert gemini_schema.properties["list_field"].items.type == Type.STRING def test_to_gemini_schema_enum(self): openapi_schema = {"type": "string", "enum": ["a", "b", "c"]} gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.enum == ["a", "b", "c"] def test_to_gemini_schema_required(self): openapi_schema = { "type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.required == ["name"] def test_to_gemini_schema_nested_dict(self): openapi_schema = { "type": "object", "properties": { "metadata": { "type": "object", "properties": { "key1": {"type": "object"}, "key2": {"type": "string"}, }, } }, } gemini_schema = _to_gemini_schema(openapi_schema) # Since metadata is not properties nor item, it will call to_gemini_schema recursively. assert isinstance(gemini_schema.properties["metadata"], Schema) assert ( gemini_schema.properties["metadata"].type == Type.OBJECT ) # add object type by default assert len(gemini_schema.properties["metadata"].properties) == 2 assert ( gemini_schema.properties["metadata"].properties["key1"].type == Type.OBJECT ) assert ( gemini_schema.properties["metadata"].properties["key2"].type == Type.STRING ) def test_to_gemini_schema_converts_property_dict(self): openapi_schema = { "properties": { "name": {"type": "string", "description": "The property key"}, "value": {"type": "string", "description": "The property value"}, }, "type": "object", "description": "A single property entry in the Properties message.", } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["name"].type == Type.STRING assert gemini_schema.properties["value"].type == Type.STRING def test_to_gemini_schema_remove_unrecognized_fields(self): openapi_schema = { "type": "string", "description": "A single date string.", "format": "date", } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.STRING assert not gemini_schema.format def test_to_gemini_schema_nested_dict_with_defs_and_ref(self): """Test that nested dict with $defs and $refs is converted correctly.""" openapi_schema = { "$defs": { "DeviceEnum": { "enum": ["GLOBAL", "desktop", "mobile"], "title": "DeviceEnum", "type": "string", }, "DomainPayload": { "properties": { "adDomain": { "description": "List of one or many domains.", "items": {"type": "string"}, "title": "Addomain", "type": "array", }, "device": { "$ref": "#/$defs/DeviceEnum", "default": "GLOBAL", "description": ( "Filter by device. All devices are returned by" " default." ), }, }, "required": ["adDomain"], "title": "DomainPayload", "type": "object", }, }, "properties": {"payload": {"$ref": "#/$defs/DomainPayload"}}, "required": ["payload"], "title": "query_domainsArguments", "type": "object", } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["payload"].type == Type.OBJECT assert ( gemini_schema.properties["payload"].properties["adDomain"].type == Type.ARRAY ) assert ( gemini_schema.properties["payload"].properties["adDomain"].items.type == Type.STRING ) assert ( gemini_schema.properties["payload"].properties["device"].type == Type.STRING ) assert gemini_schema.properties["payload"].properties["device"].enum == [ "GLOBAL", "desktop", "mobile", ] assert gemini_schema.properties["payload"].required == ["adDomain"] def test_to_gemini_schema_draft_07_definitions_and_ref(self): """Draft-07 schemas use `definitions`/`#/definitions/...` instead of `$defs`. The MCP spec allows tool `inputSchema`s to use JSON Schema draft-07, so a server sending `definitions` + `$ref: "#/definitions/..."` must dereference correctly instead of raising `KeyError: 'definitions'`. """ openapi_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "DeviceEnum": { "enum": ["GLOBAL", "desktop", "mobile"], "title": "DeviceEnum", "type": "string", }, "DomainPayload": { "properties": { "adDomain": { "description": "List of one or many domains.", "items": {"type": "string"}, "title": "Addomain", "type": "array", }, "device": { "$ref": "#/definitions/DeviceEnum", "default": "GLOBAL", }, }, "required": ["adDomain"], "title": "DomainPayload", "type": "object", }, }, "properties": {"payload": {"$ref": "#/definitions/DomainPayload"}}, "required": ["payload"], "title": "query_domainsArguments", "type": "object", } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["payload"].type == Type.OBJECT assert ( gemini_schema.properties["payload"].properties["adDomain"].type == Type.ARRAY ) assert ( gemini_schema.properties["payload"].properties["adDomain"].items.type == Type.STRING ) assert ( gemini_schema.properties["payload"].properties["device"].type == Type.STRING ) assert gemini_schema.properties["payload"].properties["device"].enum == [ "GLOBAL", "desktop", "mobile", ] assert gemini_schema.properties["payload"].required == ["adDomain"] def test_sanitize_integer_formats(self): """Test that int32 and int64 formats are preserved for integer types""" openapi_schema = { "type": "object", "properties": { "int32_field": {"type": "integer", "format": "int32"}, "int64_field": {"type": "integer", "format": "int64"}, "invalid_int_format": {"type": "integer", "format": "unsigned"}, }, } gemini_schema = _to_gemini_schema(openapi_schema) # int32 and int64 should be preserved assert gemini_schema.properties["int32_field"].format == "int32" assert gemini_schema.properties["int64_field"].format == "int64" # Invalid format should be removed assert gemini_schema.properties["invalid_int_format"].format is None def test_sanitize_string_formats(self): """Test that only date-time and enum formats are preserved for string types""" openapi_schema = { "type": "object", "properties": { "datetime_field": {"type": "string", "format": "date-time"}, "enum_field": { "type": "string", "format": "enum", "enum": ["a", "b"], }, "date_field": {"type": "string", "format": "date"}, "email_field": {"type": "string", "format": "email"}, "byte_field": {"type": "string", "format": "byte"}, }, } gemini_schema = _to_gemini_schema(openapi_schema) # date-time and enum should be preserved assert gemini_schema.properties["datetime_field"].format == "date-time" assert gemini_schema.properties["enum_field"].format == "enum" # Other formats should be removed assert gemini_schema.properties["date_field"].format is None assert gemini_schema.properties["email_field"].format is None assert gemini_schema.properties["byte_field"].format is None def test_sanitize_number_formats(self): """Test format handling for number types""" openapi_schema = { "type": "object", "properties": { "float_field": {"type": "number", "format": "float"}, "double_field": {"type": "number", "format": "double"}, "int32_number": {"type": "number", "format": "int32"}, }, } gemini_schema = _to_gemini_schema(openapi_schema) # float and double should be removed for number type assert gemini_schema.properties["float_field"].format is None assert gemini_schema.properties["double_field"].format is None # int32 should be preserved even for number type assert gemini_schema.properties["int32_number"].format == "int32" def test_sanitize_nested_formats(self): """Test format sanitization in nested structures""" openapi_schema = { "type": "object", "properties": { "nested": { "type": "object", "properties": { "date_str": {"type": "string", "format": "date"}, "int_field": {"type": "integer", "format": "int64"}, }, }, "array_field": { "type": "array", "items": {"type": "string", "format": "uri"}, }, }, } gemini_schema = _to_gemini_schema(openapi_schema) # Check nested object assert ( gemini_schema.properties["nested"].properties["date_str"].format is None ) assert ( gemini_schema.properties["nested"].properties["int_field"].format == "int64" ) # Check array items assert gemini_schema.properties["array_field"].items.format is None def test_sanitize_anyof_formats(self): """Test format sanitization in anyOf structures""" openapi_schema = { "anyOf": [ {"type": "string", "format": "email"}, {"type": "integer", "format": "int32"}, {"type": "string", "format": "date-time"}, ], } gemini_schema = _to_gemini_schema(openapi_schema) # First anyOf should have format removed (email) assert gemini_schema.any_of[0].format is None # Second anyOf should preserve int32 assert gemini_schema.any_of[1].format == "int32" # Third anyOf should preserve date-time assert gemini_schema.any_of[2].format == "date-time" def test_camel_case_to_snake_case_conversion(self): """Test that camelCase keys are converted to snake_case""" openapi_schema = { "type": "object", "minProperties": 1, "maxProperties": 10, "properties": { "firstName": {"type": "string", "minLength": 1, "maxLength": 50}, "lastName": {"type": "string", "minLength": 1, "maxLength": 50}, }, } gemini_schema = _to_gemini_schema(openapi_schema) # Check snake_case conversion assert gemini_schema.min_properties == 1 assert gemini_schema.max_properties == 10 assert gemini_schema.properties["firstName"].min_length == 1 assert gemini_schema.properties["firstName"].max_length == 50 def test_preserve_valid_formats_without_type(self): """Test behavior when format is specified but type is missing""" openapi_schema = { "format": "date-time", # No type specified "properties": { "field1": {"format": "int32"}, # No type }, } gemini_schema = _to_gemini_schema(openapi_schema) # Format should be removed when type is not specified assert gemini_schema.format is None assert gemini_schema.properties["field1"].format is None def test_to_gemini_schema_property_ordering(self): openapi_schema = { "type": "object", "propertyOrdering": ["name", "age"], "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, }, } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.property_ordering == ["name", "age"] def test_sanitize_schema_formats_for_gemini(self): schema = { "type": "object", "description": "Test schema", # Top-level description "properties": { "valid_int": {"type": "integer", "format": "int32"}, "invalid_format_prop": {"type": "integer", "format": "unsigned"}, "valid_string": {"type": "string", "format": "date-time"}, "camelCaseKey": {"type": "string"}, "prop_with_extra_key": { "type": "boolean", "unknownInternalKey": "discard_this_value", }, }, "required": ["valid_int"], "additionalProperties": False, # This is an unsupported top-level key "unknownTopLevelKey": ( "discard_me_too" ), # Another unsupported top-level key } sanitized = _sanitize_schema_formats_for_gemini(schema) # Check description is preserved assert sanitized["description"] == "Test schema" # Check properties and their sanitization assert "properties" in sanitized sanitized_props = sanitized["properties"] assert "valid_int" in sanitized_props assert sanitized_props["valid_int"]["type"] == "integer" assert sanitized_props["valid_int"]["format"] == "int32" assert "invalid_format_prop" in sanitized_props assert sanitized_props["invalid_format_prop"]["type"] == "integer" assert ( "format" not in sanitized_props["invalid_format_prop"] ) # Invalid format removed assert "valid_string" in sanitized_props assert sanitized_props["valid_string"]["type"] == "string" assert sanitized_props["valid_string"]["format"] == "date-time" # Check camelCase keys not changed for properties assert "camel_case_key" not in sanitized_props assert "camelCaseKey" in sanitized_props assert sanitized_props["camelCaseKey"]["type"] == "string" # Check removal of unsupported keys within a property definition assert "prop_with_extra_key" in sanitized_props assert sanitized_props["prop_with_extra_key"]["type"] == "boolean" assert ( "unknown_internal_key" # snake_cased version of unknownInternalKey not in sanitized_props["prop_with_extra_key"] ) # Check removal of unsupported top-level fields (after snake_casing) assert "additional_properties" not in sanitized assert "unknown_top_level_key" not in sanitized # Check original unsupported top-level field names are not there either assert "additionalProperties" not in sanitized assert "unknownTopLevelKey" not in sanitized # Check required is preserved assert sanitized["required"] == ["valid_int"] # Test with a schema that has a list of types for a property schema_with_list_type = { "type": "object", "properties": { "nullable_field": {"type": ["string", "null"], "format": "uuid"} }, } sanitized_list_type = _sanitize_schema_formats_for_gemini( schema_with_list_type ) # format should be removed because 'uuid' is not supported for string assert "format" not in sanitized_list_type["properties"]["nullable_field"] # type should be processed by _sanitize_schema_type and preserved assert sanitized_list_type["properties"]["nullable_field"]["type"] == [ "string", "null", ] def test_sanitize_schema_formats_for_gemini_with_list_property_value(self): schema = { "type": "object", "properties": { "required": ["sql"], "sql": {"type": "string"}, }, } sanitized = _sanitize_schema_formats_for_gemini(schema) assert sanitized["properties"]["required"] == ["sql"] assert sanitized["properties"]["sql"]["type"] == "string" def test_sanitize_schema_formats_for_gemini_nullable(self): openapi_schema = { "properties": { "case_id": { "description": "The ID of the case.", "title": "Case Id", "type": "string", }, "next_page_token": { "any_of": [ {"type": "string"}, {"type": ["object", "null"]}, ], "description": ( "The nextPageToken to fetch the next page of results." ), "title": "Next Page Token", }, }, "required": ["case_id"], "title": "list_alerts_by_caseArguments", "type": "object", } openapi_schema = _sanitize_schema_formats_for_gemini(openapi_schema) assert openapi_schema == { "properties": { "case_id": { "description": "The ID of the case.", "title": "Case Id", "type": "string", }, "next_page_token": { "any_of": [ {"type": "string"}, {"type": ["object", "null"]}, ], "description": ( "The nextPageToken to fetch the next page of results." ), "title": "Next Page Token", }, }, "required": ["case_id"], "title": "list_alerts_by_caseArguments", "type": "object", } def test_to_gemini_schema_properties_is_none(self): """Tests schema conversion when 'properties' field is None.""" openapi_schema = {"type": "object", "properties": None} gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties is None def test_to_gemini_schema_boolean_true_property(self): """Tests that a JSON Schema boolean `true` property is handled. JSON Schema allows `true` as a schema meaning "accept any value". Some MCP servers use this pattern for fields whose content is not further constrained. """ openapi_schema = { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "refId": {"type": "string"}, "model": True, # JSON Schema boolean schema }, }, } }, } gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) items_schema = gemini_schema.properties["items"] assert items_schema.type == Type.ARRAY # `model: true` should be converted to an object schema model_schema = items_schema.items.properties["model"] assert model_schema.type == Type.OBJECT def test_to_gemini_schema_boolean_false_property(self): """Tests that a JSON Schema boolean `false` property does not raise. `false` means "no value is valid" in JSON Schema, which has no Gemini equivalent. Conversion falls back to an object schema to avoid crashing; the result is semantically imprecise but safe. """ openapi_schema = { "type": "object", "properties": { "anything": False, # JSON Schema boolean schema (reject all) }, } # Should not raise even though `false` has no Gemini equivalent. gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) assert gemini_schema.properties["anything"] is not None def test_to_gemini_schema_boolean_true_in_array_items_properties(self): """Regression test: boolean `true` schema inside array item properties. Some MCP servers use `"field": true` in an array item's properties to indicate an unconstrained field, which is valid JSON Schema. """ openapi_schema = { "type": "object", "properties": { "title": {"type": "string"}, "data": { "type": "array", "items": { "type": "object", "properties": { "datasourceUid": {"type": "string"}, "model": True, "queryType": {"type": "string"}, "refId": {"type": "string"}, }, }, }, }, "required": ["title", "data"], } # Should not raise a ValidationError gemini_schema = _to_gemini_schema(openapi_schema) assert isinstance(gemini_schema, Schema) assert gemini_schema.type == Type.OBJECT data_schema = gemini_schema.properties["data"] assert data_schema.type == Type.ARRAY model_schema = data_schema.items.properties["model"] assert model_schema.type == Type.OBJECT def test_to_gemini_schema_circular_ref(self): """Test that circular references in schema are handled without RecursionError.""" openapi_schema = { "$defs": { "Node": { "type": "object", "properties": { "name": {"type": "string"}, "parent": {"$ref": "#/$defs/Node"}, }, } }, "properties": {"tree": {"$ref": "#/$defs/Node"}}, "type": "object", } # Should not raise RecursionError gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.OBJECT assert gemini_schema.properties["tree"].type == Type.OBJECT assert ( gemini_schema.properties["tree"].properties["name"].type == Type.STRING ) assert ( gemini_schema.properties["tree"].properties["parent"].type == Type.OBJECT ), "The circular ref should be handled and return the fallback object" assert ( gemini_schema.properties["tree"].properties["parent"].description == "Circular ref to Node" ) def test_to_gemini_schema_multi_step_circular_ref(self): """Test that multi-step circular references (Value -> Struct -> Value) are handled.""" openapi_schema = { "$defs": { "Value": { "anyOf": [ {"type": "string"}, {"$ref": "#/$defs/Struct"}, ] }, "Struct": { "type": "object", "properties": { "fields": { "type": "object", "properties": { "my_val": { "type": "array", "items": {"$ref": "#/$defs/Value"}, } }, } }, }, }, "properties": {"root": {"$ref": "#/$defs/Value"}}, "type": "object", } gemini_schema = _to_gemini_schema(openapi_schema) # Individual assertions are used here instead of comparing the whole Schema # object or its properties dictionary because Schema objects with deep # nesting can have subtle differences in default fields that are hard to # debug due to pytest truncation limits. assert gemini_schema.type == Type.OBJECT # root is Value, which resolved to anyOf assert len(gemini_schema.properties["root"].any_of) == 2 assert gemini_schema.properties["root"].any_of[0].type == Type.STRING # any_of[1] is Struct struct_schema = gemini_schema.properties["root"].any_of[1] assert struct_schema.type == Type.OBJECT assert struct_schema.properties["fields"].type == Type.OBJECT # properties["fields"].properties["my_val"] is an array my_val_schema = struct_schema.properties["fields"].properties["my_val"] assert my_val_schema.type == Type.ARRAY assert ( my_val_schema.items.type == Type.OBJECT ), "Array items referencing a circular $ref should resolve to Type.OBJECT" def test_to_gemini_schema_reused_non_circular_ref(self): """Test that reused non-circular references are handled correctly.""" openapi_schema = { "$defs": { "CommonType": {"type": "string"}, "ObjectA": { "type": "object", "properties": {"prop_a": {"$ref": "#/$defs/CommonType"}}, }, "ObjectB": { "type": "object", "properties": {"prop_b": {"$ref": "#/$defs/CommonType"}}, }, }, "properties": { "a": {"$ref": "#/$defs/ObjectA"}, "b": {"$ref": "#/$defs/ObjectB"}, }, "type": "object", } gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.type == Type.OBJECT assert ( gemini_schema.properties["a"].properties["prop_a"].type == Type.STRING ) assert ( gemini_schema.properties["b"].properties["prop_b"].type == Type.STRING ) class TestToSnakeCase: @pytest.mark.parametrize( "input_str, expected_output", [ ("lowerCamelCase", "lower_camel_case"), ("UpperCamelCase", "upper_camel_case"), ("space separated", "space_separated"), ("REST API", "rest_api"), ("Mixed_CASE with_Spaces", "mixed_case_with_spaces"), ("__init__", "init"), ("APIKey", "api_key"), ("SomeLongURL", "some_long_url"), ("CONSTANT_CASE", "constant_case"), ("already_snake_case", "already_snake_case"), ("single", "single"), ("", ""), (" spaced ", "spaced"), ("with123numbers", "with123numbers"), ("With_Mixed_123_and_SPACES", "with_mixed_123_and_spaces"), ("HTMLParser", "html_parser"), ("HTTPResponseCode", "http_response_code"), ("a_b_c", "a_b_c"), ("A_B_C", "a_b_c"), ("fromAtoB", "from_ato_b"), ("XMLHTTPRequest", "xmlhttp_request"), ("_leading", "leading"), ("trailing_", "trailing"), (" leading_and_trailing_ ", "leading_and_trailing"), ("Multiple___Underscores", "multiple_underscores"), (" spaces_and___underscores ", "spaces_and_underscores"), (" _mixed_Case ", "mixed_case"), ("123Start", "123_start"), ("End123", "end123"), ("Mid123dle", "mid123dle"), ], ) def test_to_snake_case(self, input_str, expected_output): assert _to_snake_case(input_str) == expected_output