From 46ce91f4f9a72a4f07d649091c6c588273ff88c6 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 14:26:47 -0700 Subject: [PATCH 1/6] add support for missing fields in schema, and add ability for schema to export to JSON schema format --- .../firebase_ai/lib/src/schema.dart | 138 +++++++++++++++++- .../firebase_ai/firebase_ai/lib/src/tool.dart | 22 +++ 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/schema.dart b/packages/firebase_ai/firebase_ai/lib/src/schema.dart index 2b0d5ce6a6bb..28348f6223ff 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/schema.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/schema.dart @@ -35,6 +35,14 @@ final class Schema { this.optionalProperties, this.propertyOrdering, this.anyOf, + this.ref, + this.minProperties, + this.maxProperties, + this.minLength, + this.maxLength, + this.pattern, + this.example, + this.defaultValue, }); /// Construct a schema for an object with one or more properties. @@ -45,6 +53,10 @@ final class Schema { String? description, String? title, bool? nullable, + int? minProperties, + int? maxProperties, + Object? example, + Object? defaultValue, }) : this( SchemaType.object, properties: properties, @@ -53,6 +65,10 @@ final class Schema { description: description, title: title, nullable: nullable, + minProperties: minProperties, + maxProperties: maxProperties, + example: example, + defaultValue: defaultValue, ); /// Construct a schema for an array of values with a specified type. @@ -63,6 +79,8 @@ final class Schema { bool? nullable, int? minItems, int? maxItems, + Object? example, + Object? defaultValue, }) : this( SchemaType.array, description: description, @@ -71,6 +89,8 @@ final class Schema { items: items, minItems: minItems, maxItems: maxItems, + example: example, + defaultValue: defaultValue, ); /// Construct a schema for bool value. @@ -78,11 +98,15 @@ final class Schema { String? description, String? title, bool? nullable, + Object? example, + Object? defaultValue, }) : this( SchemaType.boolean, description: description, title: title, nullable: nullable, + example: example, + defaultValue: defaultValue, ); /// Construct a schema for an integer number. @@ -95,6 +119,8 @@ final class Schema { String? format, int? minimum, int? maximum, + Object? example, + Object? defaultValue, }) : this( SchemaType.integer, description: description, @@ -103,6 +129,8 @@ final class Schema { format: format, minimum: minimum?.toDouble(), maximum: maximum?.toDouble(), + example: example, + defaultValue: defaultValue, ); /// Construct a schema for a non-integer number. @@ -115,6 +143,8 @@ final class Schema { String? format, double? minimum, double? maximum, + Object? example, + Object? defaultValue, }) : this( SchemaType.number, description: description, @@ -123,6 +153,8 @@ final class Schema { format: format, minimum: minimum, maximum: maximum, + example: example, + defaultValue: defaultValue, ); /// Construct a schema for String value with enumerated possible values. @@ -131,6 +163,8 @@ final class Schema { String? description, String? title, bool? nullable, + Object? example, + Object? defaultValue, }) : this( SchemaType.string, enumValues: enumValues, @@ -138,6 +172,8 @@ final class Schema { title: title, nullable: nullable, format: 'enum', + example: example, + defaultValue: defaultValue, ); /// Construct a schema for a String value. @@ -146,12 +182,22 @@ final class Schema { String? title, bool? nullable, String? format, + int? minLength, + int? maxLength, + String? pattern, + Object? example, + Object? defaultValue, }) : this( SchemaType.string, description: description, title: title, nullable: nullable, format: format, + minLength: minLength, + maxLength: maxLength, + pattern: pattern, + example: example, + defaultValue: defaultValue, ); /// Construct a schema representing a value that must conform to @@ -180,6 +226,12 @@ final class Schema { anyOf: schemas, ); + /// Construct a schema referencing another schema. + Schema.ref(String ref) : this( + SchemaType.ref, + ref: ref, + ); + /// The type of this value. SchemaType type; @@ -257,10 +309,35 @@ final class Schema { /// Schema.anyOf(schemas: [Schema.string(), Schema.integer()]); List? anyOf; + /// Reference to another schema. + String? ref; + + /// Minimum number of properties for Type.OBJECT. + int? minProperties; + + /// Maximum number of properties for Type.OBJECT. + int? maxProperties; + + /// Minimum length for Type.STRING. + int? minLength; + + /// Maximum length for Type.STRING. + int? maxLength; + + /// Pattern for Type.STRING to restrict a string to a regular expression. + String? pattern; + + /// Example of the object. + Object? example; + + /// Default value of the field. + Object? defaultValue; + /// Convert to json object. Map toJson() => { - if (type != SchemaType.anyOf) - 'type': type.toJson(), // Omit the field while type is anyOf + if (type != SchemaType.anyOf && type != SchemaType.ref) + 'type': type.toJson(), // Omit the field while type is anyOf or ref + if (ref case final ref?) r'$ref': ref, if (format case final format?) 'format': format, if (description case final description?) 'description': description, if (title case final title?) 'title': title, @@ -287,6 +364,59 @@ final class Schema { 'propertyOrdering': propertyOrdering, if (anyOf case final anyOf?) 'anyOf': anyOf.map((e) => e.toJson()).toList(), + if (minProperties case final minProperties?) + 'minProperties': minProperties, + if (maxProperties case final maxProperties?) + 'maxProperties': maxProperties, + if (minLength case final minLength?) + 'minLength': minLength, + if (maxLength case final maxLength?) + 'maxLength': maxLength, + if (pattern case final pattern?) + 'pattern': pattern, + if (example case final example?) 'example': example, + if (defaultValue case final defaultValue?) 'default': defaultValue, + }; + + /// Convert to standard JSON Schema object. + /// + /// Reference: https://ai.google.dev/api/caching#FunctionDeclaration + Map toJSONSchemaJson() => { + if (type != SchemaType.anyOf && type != SchemaType.ref) + 'type': nullable == true ? [type.name, 'null'] : type.name, + if (ref case final ref?) r'$ref': ref, + if (format case final format?) 'format': format, + if (description case final description?) 'description': description, + if (title case final title?) 'title': title, + if (enumValues case final enumValues?) 'enum': enumValues, + if (items case final items?) 'items': items.toJSONSchemaJson(), + if (minItems case final minItems?) 'minItems': minItems, + if (maxItems case final maxItems?) 'maxItems': maxItems, + if (minimum case final minimum?) 'minimum': minimum, + if (maximum case final maximum?) 'maximum': maximum, + if (properties case final properties?) + 'properties': { + for (final MapEntry(:key, :value) in properties.entries) + key: value.toJSONSchemaJson() + }, + // Calculate required properties based on optionalProperties + if (properties != null) + 'required': optionalProperties != null + ? properties!.keys + .where((key) => !optionalProperties!.contains(key)) + .toList() + : properties!.keys.toList(), + if (anyOf case final anyOf?) + 'anyOf': anyOf.map((e) => e.toJSONSchemaJson()).toList(), + if (minProperties case final minProperties?) + 'minProperties': minProperties, + if (maxProperties case final maxProperties?) + 'maxProperties': maxProperties, + if (minLength case final minLength?) 'minLength': minLength, + if (maxLength case final maxLength?) 'maxLength': maxLength, + if (pattern case final pattern?) 'pattern': pattern, + if (example case final example?) 'examples': [example], + if (defaultValue case final defaultValue?) 'default': defaultValue, }; } @@ -310,6 +440,9 @@ enum SchemaType { /// object type object, + /// This schema is a reference type. + ref, + /// This schema is anyOf type. anyOf; @@ -321,6 +454,7 @@ enum SchemaType { boolean => 'BOOLEAN', array => 'ARRAY', object => 'OBJECT', + ref => 'null', anyOf => 'null', }; } diff --git a/packages/firebase_ai/firebase_ai/lib/src/tool.dart b/packages/firebase_ai/firebase_ai/lib/src/tool.dart index 6b92fbd5d96c..35fc6a15d64d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/tool.dart @@ -118,6 +118,19 @@ final class Tool { if (_urlContext case final _urlContext?) 'urlContext': _urlContext.toJson(), }; + + /// Reference: https://ai.google.dev/api/caching#FunctionDeclaration + Map toJSONSchemaJson() => { + if (_functionDeclarations case final _functionDeclarations?) + 'functionDeclarations': + _functionDeclarations.map((f) => f.toJSONSchemaJson()).toList(), + if (_googleSearch case final _googleSearch?) + 'googleSearch': _googleSearch.toJson(), + if (_codeExecution case final _codeExecution?) + 'codeExecution': _codeExecution.toJson(), + if (_urlContext case final _urlContext?) + 'urlContext': _urlContext.toJson(), + }; } /// A tool that allows the generative model to connect to Google Search to @@ -194,6 +207,15 @@ class FunctionDeclaration { 'description': description, 'parameters': _schemaObject.toJson() }; + + /// Convert schema object to JSON schema. + /// + /// Reference: https://ai.google.dev/api/caching#FunctionDeclaration + Map toJSONSchemaJson() => { + 'name': name, + 'description': description, + 'parametersJsonSchema': _schemaObject.toJson() + }; } /// A [FunctionDeclaration] for auto function calling. From bc525cf5b7bc6fd683c69aafd40771133b582ef5 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 14:38:27 -0700 Subject: [PATCH 2/6] add unit tests --- .../firebase_ai/test/schema_test.dart | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/test/schema_test.dart b/packages/firebase_ai/firebase_ai/test/schema_test.dart index e4b47be4be94..a44c7441ac4f 100644 --- a/packages/firebase_ai/firebase_ai/test/schema_test.dart +++ b/packages/firebase_ai/firebase_ai/test/schema_test.dart @@ -253,10 +253,96 @@ void main() { expect(SchemaType.boolean.toJson(), 'BOOLEAN'); expect(SchemaType.array.toJson(), 'ARRAY'); expect(SchemaType.object.toJson(), 'OBJECT'); + expect(SchemaType.ref.toJson(), 'null'); expect(SchemaType.anyOf.toJson(), 'null'); // As per implementation, 'null' string for anyOf }); + // Test Schema.ref + test('Schema.ref', () { + final schema = Schema.ref('#/components/schemas/User'); + expect(schema.type, SchemaType.ref); + expect(schema.ref, '#/components/schemas/User'); + expect(schema.toJson(), { + r'$ref': '#/components/schemas/User', + }); + expect(schema.toJSONSchemaJson(), { + r'$ref': '#/components/schemas/User', + }); + }); + + // Test new constraints & keywords + test('Schema.string with new constraints', () { + final schema = Schema.string( + minLength: 5, + maxLength: 10, + pattern: r'^[a-z]+$', + example: 'hello', + defaultValue: 'test'); + expect(schema.minLength, 5); + expect(schema.maxLength, 10); + expect(schema.pattern, r'^[a-z]+$'); + expect(schema.example, 'hello'); + expect(schema.defaultValue, 'test'); + expect(schema.toJson(), { + 'type': 'STRING', + 'minLength': 5, + 'maxLength': 10, + 'pattern': r'^[a-z]+$', + 'example': 'hello', + 'default': 'test', + }); + expect(schema.toJSONSchemaJson(), { + 'type': 'string', + 'minLength': 5, + 'maxLength': 10, + 'pattern': r'^[a-z]+$', + 'examples': ['hello'], + 'default': 'test', + }); + }); + + test('Schema.object with new constraints', () { + final schema = Schema.object( + properties: {}, + minProperties: 1, + maxProperties: 5, + example: {'a': 1}, + defaultValue: {'a': 2}); + expect(schema.minProperties, 1); + expect(schema.maxProperties, 5); + expect(schema.toJson(), { + 'type': 'OBJECT', + 'properties': {}, + 'required': [], + 'minProperties': 1, + 'maxProperties': 5, + 'example': {'a': 1}, + 'default': {'a': 2}, + }); + expect(schema.toJSONSchemaJson(), { + 'type': 'object', + 'properties': {}, + 'required': [], + 'minProperties': 1, + 'maxProperties': 5, + 'examples': [{'a': 1}], + 'default': {'a': 2}, + }); + }); + + test('toJSONSchemaJson handles nullable correctly', () { + final schema = Schema.integer(nullable: true); + expect(schema.toJSONSchemaJson(), { + 'type': ['integer', 'null'], + }); + + final stringSchema = Schema.string(nullable: false); + expect(stringSchema.toJSONSchemaJson(), { + 'type': 'string', + }); + }); + // Test edge cases test('Schema.object with no properties', () { final schema = Schema.object(properties: {}); @@ -267,6 +353,11 @@ void main() { 'properties': {}, 'required': [], }); + expect(schema.toJSONSchemaJson(), { + 'type': 'object', + 'properties': {}, + 'required': [], + }); }); test('Schema.array with no items (should not happen with constructor)', () { @@ -278,6 +369,10 @@ void main() { 'type': 'ARRAY', // 'items' field should be absent if items is null }); + expect(schema.toJSONSchemaJson(), { + 'type': 'array', + // 'items' field should be absent if items is null + }); }); test('Schema with all optional fields null', () { @@ -292,6 +387,35 @@ void main() { expect(schema.optionalProperties, isNull); expect(schema.anyOf, isNull); expect(schema.toJson(), {'type': 'STRING'}); + expect(schema.toJSONSchemaJson(), {'type': 'string'}); + }); + + test('Schema with recursive array referencing another schema', () { + final schema = Schema.array( + items: Schema.ref('#/components/schemas/Item'), + nullable: true, + ); + expect(schema.toJson(), { + 'type': 'ARRAY', + 'nullable': true, + 'items': {r'$ref': '#/components/schemas/Item'}, + }); + expect(schema.toJSONSchemaJson(), { + 'type': ['array', 'null'], + 'items': {r'$ref': '#/components/schemas/Item'}, + }); + }); + + test('Schema manually constructed without matching values (ref)', () { + final schema = Schema(SchemaType.ref); + expect(schema.toJson(), {}); // type is ignored, ref is null + expect(schema.toJSONSchemaJson(), {}); + }); + + test('Schema manually constructed without matching values (anyOf)', () { + final schema = Schema(SchemaType.anyOf); + expect(schema.toJson(), {}); // type is ignored, anyOf is null + expect(schema.toJSONSchemaJson(), {}); }); }); } From 2153d95422ffc50690475ef98dce301ffb4ec7c6 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 14:47:24 -0700 Subject: [PATCH 3/6] add the switch to FunctionDeclaration and unit test --- .../firebase_ai/firebase_ai/lib/src/tool.dart | 38 +++++-------- .../firebase_ai/test/tool_test.dart | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/tool.dart b/packages/firebase_ai/firebase_ai/lib/src/tool.dart index 35fc6a15d64d..228ad5a001d6 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/tool.dart @@ -118,19 +118,6 @@ final class Tool { if (_urlContext case final _urlContext?) 'urlContext': _urlContext.toJson(), }; - - /// Reference: https://ai.google.dev/api/caching#FunctionDeclaration - Map toJSONSchemaJson() => { - if (_functionDeclarations case final _functionDeclarations?) - 'functionDeclarations': - _functionDeclarations.map((f) => f.toJSONSchemaJson()).toList(), - if (_googleSearch case final _googleSearch?) - 'googleSearch': _googleSearch.toJson(), - if (_codeExecution case final _codeExecution?) - 'codeExecution': _codeExecution.toJson(), - if (_urlContext case final _urlContext?) - 'urlContext': _urlContext.toJson(), - }; } /// A tool that allows the generative model to connect to Google Search to @@ -186,7 +173,8 @@ class FunctionDeclaration { // ignore: public_member_api_docs FunctionDeclaration(this.name, this.description, {required Map parameters, - List optionalParameters = const []}) + List optionalParameters = const [], + this.useJSONSchema = false}) : _schemaObject = Schema.object( properties: parameters, optionalProperties: optionalParameters); @@ -199,22 +187,19 @@ class FunctionDeclaration { /// A brief description of the function. final String description; + /// Whether to output parameters using [Schema.toJSONSchemaJson]. + final bool useJSONSchema; + final Schema _schemaObject; /// Convert to json object. Map toJson() => { 'name': name, 'description': description, - 'parameters': _schemaObject.toJson() - }; - - /// Convert schema object to JSON schema. - /// - /// Reference: https://ai.google.dev/api/caching#FunctionDeclaration - Map toJSONSchemaJson() => { - 'name': name, - 'description': description, - 'parametersJsonSchema': _schemaObject.toJson() + if (useJSONSchema) + 'parametersJsonSchema': _schemaObject.toJSONSchemaJson() + else + 'parameters': _schemaObject.toJson(), }; } @@ -232,9 +217,12 @@ final class AutoFunctionDeclaration extends FunctionDeclaration { required String description, required Map parameters, List optionalParameters = const [], + bool useJSONSchema = false, required this.callable, }) : super(name, description, - parameters: parameters, optionalParameters: optionalParameters); + parameters: parameters, + optionalParameters: optionalParameters, + useJSONSchema: useJSONSchema); /// The callable function that this declaration represents. final FutureOr> Function(Map args) diff --git a/packages/firebase_ai/firebase_ai/test/tool_test.dart b/packages/firebase_ai/firebase_ai/test/tool_test.dart index affd00691b59..ef78242344fc 100644 --- a/packages/firebase_ai/firebase_ai/test/tool_test.dart +++ b/packages/firebase_ai/firebase_ai/test/tool_test.dart @@ -113,6 +113,59 @@ void main() { expect(resultWithName, {'message': 'Hello, Bob!'}); }); + test('AutoFunctionDeclaration with useJSONSchema true', () async { + final parametersSchema = { + 'count': Schema.integer(), + }; + + final autoDeclaration = AutoFunctionDeclaration( + name: 'testSchema', + description: 'Tests JSON Schema output.', + parameters: parametersSchema, + useJSONSchema: true, + callable: (args) async => {'result': args['count']}, + ); + + expect(autoDeclaration.useJSONSchema, isTrue); + expect(autoDeclaration.toJson(), { + 'name': 'testSchema', + 'description': 'Tests JSON Schema output.', + 'parametersJsonSchema': { + 'type': 'object', + 'properties': { + 'count': {'type': 'integer'}, + }, + 'required': ['count'], + }, + }); + }); + + test('FunctionDeclaration with useJSONSchema true', () { + final parametersSchema = { + 'count': Schema.integer(), + }; + + final declaration = FunctionDeclaration( + 'testSchema', + 'Tests JSON Schema output.', + parameters: parametersSchema, + useJSONSchema: true, + ); + + expect(declaration.useJSONSchema, isTrue); + expect(declaration.toJson(), { + 'name': 'testSchema', + 'description': 'Tests JSON Schema output.', + 'parametersJsonSchema': { + 'type': 'object', + 'properties': { + 'count': {'type': 'integer'}, + }, + 'required': ['count'], + }, + }); + }); + // Test FunctionCallingConfig test('FunctionCallingConfig.auto()', () { final config = FunctionCallingConfig.auto(); From 93a0a0845a30fac0471ad3ac0b17fdf536c1c614 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 15:09:18 -0700 Subject: [PATCH 4/6] Add test case in function calling page --- .../firebase_ai/example/ios/Podfile | 1 - .../contents.xcworkspacedata | 3 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 -- .../xcshareddata/WorkspaceSettings.xcsettings | 8 -- .../lib/pages/function_calling_page.dart | 88 +++++++++++++++++++ .../firebase_ai/example/macos/Podfile | 1 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 -- 7 files changed, 88 insertions(+), 29 deletions(-) delete mode 100644 packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 packages/firebase_ai/firebase_ai/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/firebase_ai/firebase_ai/example/ios/Podfile b/packages/firebase_ai/firebase_ai/example/ios/Podfile index a419e4239013..9d7ef08d52b2 100644 --- a/packages/firebase_ai/firebase_ai/example/ios/Podfile +++ b/packages/firebase_ai/firebase_ai/example/ios/Podfile @@ -29,7 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do diff --git a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc14c74e..1d526a16ed0f 100644 --- a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5ea15..000000000000 --- a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index e4cd355259dd..462e7d38d899 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart @@ -42,8 +42,10 @@ class _FunctionCallingPageState extends State { late GenerativeModel _functionCallModel; late GenerativeModel _autoFunctionCallModel; late GenerativeModel _parallelAutoFunctionCallModel; + late GenerativeModel _complexJSONSchemaModel; late GenerativeModel _codeExecutionModel; late final AutoFunctionDeclaration _autoFetchWeatherTool; + late final AutoFunctionDeclaration _autoPlanVacationTool; final List _messages = []; bool _loading = false; bool _enableThinking = false; @@ -113,6 +115,47 @@ class _FunctionCallingPageState extends State { return getRestaurantMenu(restaurantName); }, ); + _autoPlanVacationTool = AutoFunctionDeclaration( + name: 'planVacation', + description: + 'Plans a complex vacation itinerary combining flights, hotels, and activities.', + useJSONSchema: true, + parameters: { + 'destination': + Schema.string(description: 'The city or country to travel to.'), + 'travelers': Schema.integer( + description: 'Number of travelers.', minimum: 1, maximum: 10), + 'travelClass': Schema.enumString( + enumValues: ['ECONOMY', 'BUSINESS', 'FIRST'], + description: 'The preferred travel class.', + ), + 'budget': + Schema.number(description: 'Total budget for the trip in USD.'), + 'activities': Schema.array( + items: Schema.string(), + description: 'A list of preferred activities.', + minItems: 1, + ), + 'accommodations': Schema.object( + description: 'Hotel preferences.', + properties: { + 'hotelType': Schema.string(), + 'stars': Schema.integer(minimum: 1, maximum: 5), + 'amenities': Schema.array(items: Schema.string()), + }, + optionalProperties: ['amenities'], + ), + }, + callable: (args) async { + return { + 'status': 'SUCCESS', + 'itineraryId': 'TRIP-98765', + 'destination': args['destination'], + 'estimatedCost': 3500.0, + 'message': 'Vacation planned successfully!', + }; + }, + ); _initializeModel(); } @@ -202,6 +245,13 @@ class _FunctionCallingPageState extends State { Tool.codeExecution(), ], ); + _complexJSONSchemaModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([_autoPlanVacationTool]), + ], + ); } // This is a hypothetical API to return a fake weather data collection for @@ -375,6 +425,19 @@ class _FunctionCallingPageState extends State { ), ], ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? _testComplexJSONSchemaAutoFunctionCalling + : null, + child: const Text('Complex JSON Schema FC'), + ), + ), + ], + ), ], ), ), @@ -608,6 +671,31 @@ class _FunctionCallingPageState extends State { }); } + Future _testComplexJSONSchemaAutoFunctionCalling() async { + await _runTest(() async { + final chat = _complexJSONSchemaModel.startChat(); + const prompt = + 'I want to plan a vacation to Paris for 2 people. We want to fly Business class, our budget is 5500 USD. We want to do wine tasting and museum tours. We prefer a 4-star boutique hotel with free breakfast.'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + final response = await chat.sendMessage(Content.text(prompt)); + + final thought = response.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); + } + + if (response.text case final text?) { + _messages.add(MessageData(text: text)); + } else { + _messages.add(MessageData(text: 'No text response from model.')); + } + }); + } + void _showError(String message) { showDialog( context: context, diff --git a/packages/firebase_ai/firebase_ai/example/macos/Podfile b/packages/firebase_ai/firebase_ai/example/macos/Podfile index b52666a10389..ff5ddb3b8bdc 100644 --- a/packages/firebase_ai/firebase_ai/example/macos/Podfile +++ b/packages/firebase_ai/firebase_ai/example/macos/Podfile @@ -28,7 +28,6 @@ flutter_macos_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do diff --git a/packages/firebase_ai/firebase_ai/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ai/firebase_ai/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/firebase_ai/firebase_ai/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - From 57d4fa8ec49ec865846906ffb4bccae46a54e5c3 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 15:28:08 -0700 Subject: [PATCH 5/6] add a test to validate $ref --- .../lib/pages/function_calling_page.dart | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index 462e7d38d899..60f1bd5e2160 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart @@ -43,9 +43,11 @@ class _FunctionCallingPageState extends State { late GenerativeModel _autoFunctionCallModel; late GenerativeModel _parallelAutoFunctionCallModel; late GenerativeModel _complexJSONSchemaModel; + late GenerativeModel _refSchemaModel; late GenerativeModel _codeExecutionModel; late final AutoFunctionDeclaration _autoFetchWeatherTool; late final AutoFunctionDeclaration _autoPlanVacationTool; + late final AutoFunctionDeclaration _autoProcessTransactionTool; final List _messages = []; bool _loading = false; bool _enableThinking = false; @@ -156,6 +158,32 @@ class _FunctionCallingPageState extends State { }; }, ); + _autoProcessTransactionTool = AutoFunctionDeclaration( + name: 'processTransaction', + description: + 'Processes a financial transaction using a predefined transaction model reference.', + useJSONSchema: true, + parameters: { + 'baseTransaction': Schema.object( + description: 'The base transaction block.', + properties: { + 'amount': Schema.number(), + 'transactionId': Schema.integer(), + }, + ), + 'transaction': Schema.ref('#/properties/baseTransaction'), + }, + callable: (args) async { + final transaction = args['transaction'] as Map?; + return { + 'status': 'SUCCESS', + 'amountProcessed': transaction?['amount'], + 'transactionId': transaction?['transactionId'], + 'message': + 'Transaction processed successfully using the reference schema!', + }; + }, + ); _initializeModel(); } @@ -252,6 +280,13 @@ class _FunctionCallingPageState extends State { Tool.functionDeclarations([_autoPlanVacationTool]), ], ); + _refSchemaModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([_autoProcessTransactionTool]), + ], + ); } // This is a hypothetical API to return a fake weather data collection for @@ -436,6 +471,15 @@ class _FunctionCallingPageState extends State { child: const Text('Complex JSON Schema FC'), ), ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? _testRefSchemaAutoFunctionCalling + : null, + child: const Text('Ref Schema FC'), + ), + ), ], ), ], @@ -696,6 +740,31 @@ class _FunctionCallingPageState extends State { }); } + Future _testRefSchemaAutoFunctionCalling() async { + await _runTest(() async { + final chat = _refSchemaModel.startChat(); + const prompt = + r'Process a transaction of \$50.00 for a pair of shoes. The transaction ID is 98765.'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + final response = await chat.sendMessage(Content.text(prompt)); + + final thought = response.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); + } + + if (response.text case final text?) { + _messages.add(MessageData(text: text)); + } else { + _messages.add(MessageData(text: 'No text response from model.')); + } + }); + } + void _showError(String message) { showDialog( context: context, From fb3f1c2b533d4364ff9790a73d2d65f6520df95a Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Sat, 21 Mar 2026 16:58:14 -0700 Subject: [PATCH 6/6] Update packages/firebase_ai/firebase_ai/lib/src/schema.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/firebase_ai/firebase_ai/lib/src/schema.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/lib/src/schema.dart b/packages/firebase_ai/firebase_ai/lib/src/schema.dart index 28348f6223ff..a33a08cc1cd4 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/schema.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/schema.dart @@ -328,6 +328,9 @@ final class Schema { String? pattern; /// Example of the object. + /// + /// When using [toJSONSchemaJson], this will be serialized as a + /// single-element array under the `examples` key. Object? example; /// Default value of the field.