@@ -864,3 +864,114 @@ private enum TestGeminiOutput: SchemaProviding {
864864 . object( properties: [ " value " : . string( ) ] , required: [ " value " ] )
865865 }
866866}
867+
868+ // MARK: - GeminiSchema Tests
869+
870+ struct GeminiSchemaTests {
871+ /// Recursively asserts that no dictionary in the tree contains the key
872+ /// `additionalProperties`.
873+ private func assertNoAdditionalProperties(
874+ _ value: Any , path: String = " $ "
875+ ) {
876+ if let dict = value as? [ String : Any ] {
877+ if dict [ " additionalProperties " ] != nil {
878+ Issue . record ( " Found additionalProperties at \( path) " )
879+ }
880+ for (key, child) in dict {
881+ assertNoAdditionalProperties ( child, path: " \( path) . \( key) " )
882+ }
883+ } else if let array = value as? [ Any ] {
884+ for (index, child) in array. enumerated ( ) {
885+ assertNoAdditionalProperties ( child, path: " \( path) [ \( index) ] " )
886+ }
887+ }
888+ }
889+
890+ @Test
891+ func stripsAdditionalPropertiesRecursively( ) throws {
892+ // Exercises every nesting path: top-level object, nested object,
893+ // array items, anyOf variants, and deeply nested combinations.
894+ let schema = GeminiSchema (
895+ . object(
896+ properties: [
897+ " name " : . string( description: " User name " ) ,
898+ " age " : . integer( description: " Age " ) ,
899+ " score " : . number( ) ,
900+ " active " : . boolean( ) ,
901+ " role " : . string( enumValues: [ " admin " , " user " ] ) ,
902+ " address " : . object(
903+ properties: [ " city " : . string( ) , " zip " : . string( ) ] ,
904+ required: [ " city " ]
905+ ) ,
906+ " items " : . array( items:
907+ . object(
908+ properties: [
909+ " meta " : . object(
910+ properties: [ " key " : . string( ) ] ,
911+ required: [ " key " ]
912+ )
913+ ] ,
914+ required: [ " meta " ]
915+ ) ) ,
916+ " optional_field " : . anyOf( [
917+ . object( properties: [ " a " : . string( ) ] , required: [ " a " ] ) ,
918+ . null
919+ ] )
920+ ] ,
921+ required: [ " name " ] ,
922+ description: " User record "
923+ )
924+ )
925+ let data = try JSONEncoder ( ) . encode ( schema)
926+ let json = try #require( JSONSerialization . jsonObject ( with: data) as? [ String : Any ] )
927+
928+ // No additionalProperties anywhere in the tree
929+ assertNoAdditionalProperties ( json)
930+
931+ // Spot-check that schema fields are still preserved
932+ #expect( json [ " type " ] as? String == " object " )
933+ #expect( json [ " description " ] as? String == " User record " )
934+ #expect( json [ " required " ] as? [ String ] == [ " name " ] )
935+
936+ let props = json [ " properties " ] as? [ String : Any ]
937+ #expect( ( props ? [ " name " ] as? [ String : Any ] ) ? [ " type " ] as? String == " string " )
938+ #expect( ( props ? [ " age " ] as? [ String : Any ] ) ? [ " type " ] as? String == " integer " )
939+ #expect( ( props ? [ " score " ] as? [ String : Any ] ) ? [ " type " ] as? String == " number " )
940+ #expect( ( props ? [ " active " ] as? [ String : Any ] ) ? [ " type " ] as? String == " boolean " )
941+ #expect( ( props ? [ " role " ] as? [ String : Any ] ) ? [ " enum " ] as? [ String ] == [ " admin " , " user " ] )
942+
943+ let address = props ? [ " address " ] as? [ String : Any ]
944+ #expect( address ? [ " type " ] as? String == " object " )
945+ }
946+
947+ @Test
948+ func geminiRequestToolSchemaOmitsAdditionalProperties( ) throws {
949+ let client = GeminiClient ( apiKey: " test-key " , model: " gemini-2.5-pro " )
950+ let tools = [
951+ ToolDefinition (
952+ name: " create_user " ,
953+ description: " Create a user " ,
954+ parametersSchema: . object(
955+ properties: [
956+ " name " : . string( ) ,
957+ " address " : . object(
958+ properties: [ " street " : . string( ) , " city " : . string( ) ] ,
959+ required: [ " street " , " city " ]
960+ ) ,
961+ " tags " : . array( items: . string( ) )
962+ ] ,
963+ required: [ " name " , " address " ]
964+ )
965+ )
966+ ]
967+ let request = try client. buildRequest ( messages: [ . user( " Hi " ) ] , tools: tools)
968+ let json = try encodeRequest ( request)
969+
970+ let jsonTools = json [ " tools " ] as? [ [ String : Any ] ]
971+ let decls = jsonTools ? [ 0 ] [ " functionDeclarations " ] as? [ [ String : Any ] ]
972+ let params = try #require( decls ? [ 0 ] [ " parameters " ] as? [ String : Any ] )
973+
974+ assertNoAdditionalProperties ( params)
975+ #expect( params [ " type " ] as? String == " object " )
976+ }
977+ }
0 commit comments