@@ -730,6 +730,115 @@ def test_to_gemini_schema_boolean_true_in_array_items_properties(self):
730730 model_schema = data_schema .items .properties ["model" ]
731731 assert model_schema .type == Type .OBJECT
732732
733+ def test_to_gemini_schema_circular_ref (self ):
734+ """Test that circular references in schema are handled without RecursionError."""
735+ openapi_schema = {
736+ "$defs" : {
737+ "Node" : {
738+ "type" : "object" ,
739+ "properties" : {
740+ "name" : {"type" : "string" },
741+ "parent" : {"$ref" : "#/$defs/Node" },
742+ },
743+ }
744+ },
745+ "properties" : {"tree" : {"$ref" : "#/$defs/Node" }},
746+ "type" : "object" ,
747+ }
748+ # Should not raise RecursionError
749+ gemini_schema = _to_gemini_schema (openapi_schema )
750+ assert gemini_schema .type == Type .OBJECT
751+ assert gemini_schema .properties ["tree" ].type == Type .OBJECT
752+ assert (
753+ gemini_schema .properties ["tree" ].properties ["name" ].type == Type .STRING
754+ )
755+ assert (
756+ gemini_schema .properties ["tree" ].properties ["parent" ].type
757+ == Type .OBJECT
758+ ), "The circular ref should be handled and return the fallback object"
759+ assert (
760+ gemini_schema .properties ["tree" ].properties ["parent" ].description
761+ == "Circular ref to Node"
762+ )
763+
764+ def test_to_gemini_schema_multi_step_circular_ref (self ):
765+ """Test that multi-step circular references (Value -> Struct -> Value) are handled."""
766+ openapi_schema = {
767+ "$defs" : {
768+ "Value" : {
769+ "anyOf" : [
770+ {"type" : "string" },
771+ {"$ref" : "#/$defs/Struct" },
772+ ]
773+ },
774+ "Struct" : {
775+ "type" : "object" ,
776+ "properties" : {
777+ "fields" : {
778+ "type" : "object" ,
779+ "properties" : {
780+ "my_val" : {
781+ "type" : "array" ,
782+ "items" : {"$ref" : "#/$defs/Value" },
783+ }
784+ },
785+ }
786+ },
787+ },
788+ },
789+ "properties" : {"root" : {"$ref" : "#/$defs/Value" }},
790+ "type" : "object" ,
791+ }
792+
793+ gemini_schema = _to_gemini_schema (openapi_schema )
794+ # Individual assertions are used here instead of comparing the whole Schema
795+ # object or its properties dictionary because Schema objects with deep
796+ # nesting can have subtle differences in default fields that are hard to
797+ # debug due to pytest truncation limits.
798+ assert gemini_schema .type == Type .OBJECT
799+ # root is Value, which resolved to anyOf
800+ assert len (gemini_schema .properties ["root" ].any_of ) == 2
801+ assert gemini_schema .properties ["root" ].any_of [0 ].type == Type .STRING
802+ # any_of[1] is Struct
803+ struct_schema = gemini_schema .properties ["root" ].any_of [1 ]
804+ assert struct_schema .type == Type .OBJECT
805+ assert struct_schema .properties ["fields" ].type == Type .OBJECT
806+ # properties["fields"].properties["my_val"] is an array
807+ my_val_schema = struct_schema .properties ["fields" ].properties ["my_val" ]
808+ assert my_val_schema .type == Type .ARRAY
809+ assert (
810+ my_val_schema .items .type == Type .OBJECT
811+ ), "Array items referencing a circular $ref should resolve to Type.OBJECT"
812+
813+ def test_to_gemini_schema_reused_non_circular_ref (self ):
814+ """Test that reused non-circular references are handled correctly."""
815+ openapi_schema = {
816+ "$defs" : {
817+ "CommonType" : {"type" : "string" },
818+ "ObjectA" : {
819+ "type" : "object" ,
820+ "properties" : {"prop_a" : {"$ref" : "#/$defs/CommonType" }},
821+ },
822+ "ObjectB" : {
823+ "type" : "object" ,
824+ "properties" : {"prop_b" : {"$ref" : "#/$defs/CommonType" }},
825+ },
826+ },
827+ "properties" : {
828+ "a" : {"$ref" : "#/$defs/ObjectA" },
829+ "b" : {"$ref" : "#/$defs/ObjectB" },
830+ },
831+ "type" : "object" ,
832+ }
833+ gemini_schema = _to_gemini_schema (openapi_schema )
834+ assert gemini_schema .type == Type .OBJECT
835+ assert (
836+ gemini_schema .properties ["a" ].properties ["prop_a" ].type == Type .STRING
837+ )
838+ assert (
839+ gemini_schema .properties ["b" ].properties ["prop_b" ].type == Type .STRING
840+ )
841+
733842
734843class TestToSnakeCase :
735844
0 commit comments