Skip to content

feat(firebaseai): add responseJsonSchema to GenerationConfig #17564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
95c617d
add responseJsonSchema to GenerationConfig
gspencergoog Jul 25, 2025
bc56c22
Bump version in pubspec
gspencergoog Jul 25, 2025
ea99bc0
Merge branch 'main' into add_responseJsonSchema
gspencergoog Jul 29, 2025
1da68ae
Fix typo
gspencergoog Jul 29, 2025
fd84f6f
Merge branch 'main' into add_responseJsonSchema
gspencergoog Jul 29, 2025
2595e89
Merge branch 'main' into add_responseJsonSchema
gspencergoog Jul 30, 2025
8ff84b6
Review changes
gspencergoog Jul 30, 2025
37cbc3c
Remove string encoding.
gspencergoog Jul 31, 2025
0f578e4
Update test.
gspencergoog Jul 31, 2025
3ccc451
Fix analyzer warning
gspencergoog Jul 31, 2025
b31c159
Merge branch 'main' into add_responseJsonSchema
gspencergoog Aug 5, 2025
4b174ef
fix(firebase_ai): Expose ThinkingConfig class in firebase_ai.dart for…
14T Aug 6, 2025
d0605cf
feat(dev-api): add inlineData support to Developer API (#17600)
ryanwilson Aug 6, 2025
672e240
fix(auth, apple): Move FirebaseAuth imports to implementation files (…
ncooke3 Aug 8, 2025
e1a1a28
feat(firebaseai): make Live API working with developer API (#17503)
cynthiajoan Aug 8, 2025
5aee87f
fix(firebaseai): Fix `usageMetadata.thoughtsTokenCount` (#17608)
andrewheard Aug 8, 2025
6775e36
ci(firestore): add `--ignore-timeouts` flag for cloud firestore e2e t…
SelaseKay Aug 11, 2025
e661817
chore(release): publish packages (#17618)
Lyokone Aug 11, 2025
956f4e2
Update CONTRIBUTING.md to add API change rule (#17619)
cynthiajoan Aug 12, 2025
896b3be
Update CHANGELOG.md (#17621)
cynthiajoan Aug 13, 2025
f1e48c6
Add JSON Schema example.
gspencergoog Aug 13, 2025
a5aa6f2
Add example.
gspencergoog Aug 14, 2025
6edc8c4
Update example and rever changes to main.dart
gspencergoog Aug 14, 2025
bcf4709
Merge branch 'main' into add_responseJsonSchema
gspencergoog Aug 14, 2025
b2dcd1a
Small tweak
gspencergoog Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions packages/firebase_ai/firebase_ai/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

// Import after file is generated through flutterfire_cli.
// import 'package:firebase_ai_example/firebase_options.dart';

import 'pages/chat_page.dart';
import 'pages/audio_page.dart';
import 'pages/bidi_page.dart';
import 'pages/chat_page.dart';
import 'pages/document.dart';
import 'pages/function_calling_page.dart';
import 'pages/image_prompt_page.dart';
import 'pages/token_count_page.dart';
import 'pages/schema_page.dart';
import 'pages/imagen_page.dart';
import 'pages/document.dart';
import 'pages/json_schema_page.dart';
import 'pages/schema_page.dart';
import 'pages/token_count_page.dart';
import 'pages/video_page.dart';
import 'pages/bidi_page.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -64,11 +65,11 @@ class _GenerativeAISampleState extends State<GenerativeAISample> {
void _initializeModel(bool useVertexBackend) {
if (useVertexBackend) {
final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance);
_currentModel = vertexInstance.generativeModel(model: 'gemini-1.5-flash');
_currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash');
_currentImagenModel = _initializeImagenModel(vertexInstance);
} else {
final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
_currentModel = googleAI.generativeModel(model: 'gemini-2.0-flash');
_currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash');
_currentImagenModel = _initializeImagenModel(googleAI);
}
}
Expand Down Expand Up @@ -184,10 +185,12 @@ class _HomeScreenState extends State<HomeScreen> {
case 6:
return SchemaPromptPage(title: 'Schema Prompt', model: currentModel);
case 7:
return DocumentPage(title: 'Document Prompt', model: currentModel);
return JsonSchemaPage(title: 'JSON Schema', model: currentModel);
case 8:
return VideoPage(title: 'Video Prompt', model: currentModel);
return DocumentPage(title: 'Document Prompt', model: currentModel);
case 9:
return VideoPage(title: 'Video Prompt', model: currentModel);
case 10:
return BidiPage(
title: 'Live Stream',
model: currentModel,
Expand Down Expand Up @@ -230,6 +233,9 @@ class _HomeScreenState extends State<HomeScreen> {
onChanged: widget.onBackendChanged,
activeTrackColor: Colors.green.withValues(alpha: 0.5),
inactiveTrackColor: Colors.blueGrey.withValues(alpha: 0.5),
// Ignore until activeThumbColor is available on Flutter
// stable.
// ignore: deprecated_member_use
activeColor: Colors.green,
inactiveThumbColor: Colors.blueGrey,
),
Expand Down Expand Up @@ -302,6 +308,11 @@ class _HomeScreenState extends State<HomeScreen> {
label: 'Schema',
tooltip: 'Schema Prompt',
),
BottomNavigationBarItem(
icon: Icon(Icons.data_object),
label: 'JSON',
tooltip: 'JSON Schema',
),
BottomNavigationBarItem(
icon: Icon(Icons.edit_document),
label: 'Document',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright 2025 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.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import '../widgets/message_widget.dart';

class JsonSchemaPage extends StatefulWidget {
const JsonSchemaPage({super.key, required this.title, required this.model});

final String title;
final GenerativeModel model;

@override
State<JsonSchemaPage> createState() => _JsonSchemaPageState();
}

class _JsonSchemaPageState extends State<JsonSchemaPage> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _textController = TextEditingController();
final FocusNode _textFieldFocus = FocusNode();
final List<MessageData> _messages = <MessageData>[];
bool _loading = false;

void _scrollDown() {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(
milliseconds: 750,
),
curve: Curves.easeOutCirc,
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemBuilder: (context, idx) {
return MessageWidget(
text: _messages[idx].text,
isFromUser: _messages[idx].fromUser ?? false,
);
},
itemCount: _messages.length,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 25,
horizontal: 15,
),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: !_loading
? () async {
await _promptJsonSchemaTest();
}
: null,
child: const Text('JSON Schema Prompt'),
),
),
],
),
),
],
),
),
);
}

Future<void> _promptJsonSchemaTest() async {
setState(() {
_loading = true;
});
try {
final content = [
Content.text(
'Generate a widget hierarchy with a column containing two text widgets ',
),
];

final jsonSchema = {
r'$defs': {
'text_widget': {
r'$anchor': 'text_widget',
'type': 'object',
'properties': {
'type': {'const': 'Text'},
'text': {'type': 'string'},
},
'required': ['type', 'text'],
},
},
'type': 'object',
'properties': {
'type': {'const': 'Column'},
'children': {
'type': 'array',
'items': {
'anyOf': [
{r'$ref': '#text_widget'},
{
'type': 'object',
'properties': {
'type': {'const': 'Row'},
'children': {
'type': 'array',
'items': {r'$ref': '#text_widget'},
},
},
'required': ['type', 'children'],
}
],
},
},
},
'required': ['type', 'children'],
};

final response = await widget.model.generateContent(
content,
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
responseJsonSchema: jsonSchema,
),
);

var text = const JsonEncoder.withIndent(' ')
.convert(json.decode(response.text ?? '') as Object?);
_messages.add(MessageData(text: '```json$text```', fromUser: false));

setState(() {
_loading = false;
_scrollDown();
});
} catch (e) {
_showError(e.toString());
setState(() {
_loading = false;
});
} finally {
_textController.clear();
setState(() {
_loading = false;
});
_textFieldFocus.requestFocus();
}
}

void _showError(String message) {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Something went wrong'),
content: SingleChildScrollView(
child: SelectableText(message),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import '../widgets/message_widget.dart';
Expand Down Expand Up @@ -132,13 +134,14 @@ class _SchemaPromptPageState extends State<SchemaPromptPage> {
),
);

var text = response.text;
_messages.add(MessageData(text: text, fromUser: false));

if (text == null) {
if (response.text == null) {
_showError('No response from API.');
return;
} else {
final text = const JsonEncoder.withIndent(' ')
.convert(json.decode(response.text!) as Object?);
_messages
.add(MessageData(text: '```json\n$text\n```', fromUser: false));
setState(() {
_loading = false;
_scrollDown();
Expand Down
26 changes: 25 additions & 1 deletion packages/firebase_ai/firebase_ai/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -998,8 +998,10 @@ final class GenerationConfig extends BaseGenerationConfig {
super.responseModalities,
this.responseMimeType,
this.responseSchema,
this.responseJsonSchema,
this.thinkingConfig,
});
}) : assert(responseSchema == null || responseJsonSchema == null,
'responseSchema and responseJsonSchema cannot both be set.');

/// The set of character sequences (up to 5) that will stop output generation.
///
Expand All @@ -1018,8 +1020,28 @@ final class GenerationConfig extends BaseGenerationConfig {
///
/// - Note: This only applies when the [responseMimeType] supports
/// a schema; currently this is limited to `application/json`.
///
/// Only one of [responseSchema] or [responseJsonSchema] may be specified at
/// the same time.
final Schema? responseSchema;

/// The response schema as a JSON-compatible map.
///
/// - Note: This only applies when the [responseMimeType] supports a schema;
/// currently this is limited to `application/json`.
///
/// This schema can include more advanced features of JSON than the [Schema]
/// class taken by [responseSchema] supports. See the [Gemini
/// documentation](https://ai.google.dev/api/generate-content#FIELDS.response_json_schema)
/// about the limitations of this feature.
///
/// Notably, this feature is only supported on Gemini 2.5 and later. Use
/// [responseSchema] for earlier models.
///
/// Only one of [responseSchema] or [responseJsonSchema] may be specified at
/// the same time.
final Map<String, Object?>? responseJsonSchema;

/// Config for thinking features.
///
/// An error will be returned if this field is set for models that don't
Expand All @@ -1036,6 +1058,8 @@ final class GenerationConfig extends BaseGenerationConfig {
'responseMimeType': responseMimeType,
if (responseSchema case final responseSchema?)
'responseSchema': responseSchema.toJson(),
if (responseJsonSchema case final responseJsonSchema?)
'responseJsonSchema': responseJsonSchema,
if (thinkingConfig case final thinkingConfig?)
'thinkingConfig': thinkingConfig.toJson(),
};
Expand Down
Loading
Loading