From 7bf0f48586bd6462a7aebf33c42a4078e3d8185a Mon Sep 17 00:00:00 2001 From: Shahin Saadati Date: Wed, 19 Aug 2026 13:40:19 -0700 Subject: [PATCH 1/5] Add the Kotlin tab for input and output schemas The structured-data section of agents/llm-agents had Python, TypeScript, Go and Java but no Kotlin, and adk-kotlin 0.8.0 is what makes the tab worth writing: Schema gained the twelve JSON Schema constraint fields, so a constraint can be declared rather than described in the property's description and hoped for. The snippet uses two of them, pattern and minLength, on the capital string. Two conditions come with those fields, both from the upstream KDoc rather than from guessing, and both easy to hit: - Gemini rejects a schema whose `format` is anything but int32/int64 on a number or enum/date-time on a string. - `default` must hold a JSON-native value, and serializing a Schema that sets one needs a Json whose serializersModule has a contextual serializer for `Any`; a plain Json throws. The tab also names the type, because `Schema` is ambiguous in this codebase: `com.google.adk.kt.types.Schema` is the data class LlmAgent takes, while `com.google.adk.kt.tools.Schema` is an unrelated annotation, and the GenAI SDK has a third. `kotlin_api.py sig Schema` prints two of them. Written as a region in CapitalAgent.kt rather than inline as the backlog row proposed. Every other Kotlin tab on this page transcludes from that file even though its Python and Java siblings are inline, so inline Kotlin here would break the page's own convention and give up CI compile coverage. Verified: runner.sh build and lint both PASS (JDK 17), verify_snippets L0-L6 pass, and rendering the page with the repo's markdown extensions shows the target group as [Python, TypeScript, Go, Java, Kotlin]. --- docs/agents/llm-agents.md | 19 +++++++++ .../snippets/agents/llm-agent/CapitalAgent.kt | 39 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 4a2578dece..c39f123de2 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -574,6 +574,25 @@ schema definitions. .build(); ``` +=== "Kotlin" + + The input and output schema is ADK's own `com.google.adk.kt.types.Schema`, + not the same-named type in the GenAI SDK. Since v0.8.0 it carries the JSON + Schema constraint fields, so a constraint can be declared instead of + described: `pattern`/`minLength`/`maxLength` for strings, + `minimum`/`maximum` for numbers, `minItems`/`maxItems` for arrays, plus + `format`, `nullable`, `default`, `anyOf` and `title`. + + ```kotlin + --8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example" + ``` + + Two of those fields carry conditions. Gemini rejects a schema whose `format` + is anything other than `int32`/`int64` on a number or `enum`/`date-time` on + a string. And `default` must hold a JSON-native value; serializing a + `Schema` that sets one needs a `Json` whose `serializersModule` has a + contextual serializer for `Any`, or it throws. + ### Manage agent context Control whether the agent receives the prior conversation history. diff --git a/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt b/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt index 468c64073b..153dce7024 100644 --- a/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt +++ b/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt @@ -10,6 +10,8 @@ import com.google.adk.kt.sessions.InMemorySessionService import com.google.adk.kt.types.Content import com.google.adk.kt.types.GenerateContentConfig import com.google.adk.kt.types.Part +import com.google.adk.kt.types.Schema +import com.google.adk.kt.types.Type import kotlinx.coroutines.flow.collect import kotlinx.coroutines.runBlocking @@ -81,6 +83,43 @@ fun main() = ) // --8<-- [end:gen_config] + // --8<-- [start:schema_example] + // Schema here is ADK's own com.google.adk.kt.types.Schema, not the + // same-named type in the GenAI SDK. + val capitalOutput = + Schema( + type = Type.OBJECT, + description = "Schema for capital city information.", + properties = + mapOf( + "capital" to + Schema( + type = Type.STRING, + description = "The capital city of the country.", + // JSON Schema constraints, added in adk-kotlin 0.8.0. + minLength = 1, + pattern = "^[A-Z][A-Za-z .'-]*", + ), + ), + required = listOf("capital"), + ) + + val structuredCapitalAgent = + LlmAgent( + name = "structured_capital_agent", + model = Gemini(name = "gemini-flash-latest"), + instruction = + Instruction( + "You are a Capital Information Agent. Given a country, respond ONLY " + + "with a JSON object containing the capital. " + + """Format: {"capital": "capital_name"}""", + ), + outputSchema = capitalOutput, + outputKey = "found_capital", + // Cannot use tools effectively here. + ) + // --8<-- [end:schema_example] + // --8<-- [start:full_example] val finalAgent = LlmAgent( From 6c0242b4239278ae40853b6642b7b7fdabfb5fd1 Mon Sep 17 00:00:00 2001 From: Shahin Saadati Date: Wed, 19 Aug 2026 13:53:48 -0700 Subject: [PATCH 2/5] Correct what the schema tab promises about enforcement and outputKey Review against the v0.8.0 sources found four claims a Kotlin reader would have acted on and been wrong, and one example that taught the wrong thing. - "Cannot use tools effectively here", carried over from the Python and Java tabs, is false for adk-kotlin. LlmAgent.kt:100-107 documents the opposite: with tools present the schema is applied directly on models that support both, and models that do not get a set_model_response fallback. The comment is gone and the tab says what actually happens. - The pattern in the example was the wrong constraint for the data. Under full-match semantics `^[A-Z][A-Za-z .'-]*` rejects Bogota, Brasilia, Reykjavik, San Jose and "Washington, D.C." - correct answers the model would be marked down for - and under JSON Schema's partial-match default it rejects nothing at all, since the tail may match zero characters. It now constrains a countryCode field with ^[A-Z]{2}$, where a pattern is genuinely the right tool, and the capital carries minLength/maxLength instead. The instruction was updated to ask for both fields, since it previously disagreed with the schema it was paired with. - "A constraint can be declared instead of described" implied ADK enforces the constraints. It does not: SchemaUtils reads none of the twelve fields, and its validation covers type, required, nullable, anyOf and items only. They are forwarded to the model, and the tab now says so. - With outputSchema set, outputKey does not hold text. LlmAgent.kt:360-374 stores the parsed Map, and on a validation failure logs and stores the raw string under the same key - so `state["found_capital"] as String` throws on the happy path, and nothing but the runtime type distinguishes the two outcomes. The page's bullets above the group say the text content is saved. - Only a top-level object schema is accepted, so the list of new fields, which includes minItems and maxItems, could have led a reader to a top-level array that silently fails validation. Also scoped the format note to Type.INTEGER as well as Type.NUMBER, and the default note to a hand-rolled Json, since ADK's own registers a contextual Any serializer (Serializers.kt:138). Verified: runner.sh build and lint both PASS (JDK 17), verify_snippets L0-L6 pass, and the rendered page shows the group as [Python, TypeScript, Go, Java, Kotlin] with the new bullets inside the Kotlin tab. --- docs/agents/llm-agents.md | 34 +++++++++++++------ .../snippets/agents/llm-agent/CapitalAgent.kt | 21 +++++++----- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index c39f123de2..4d093df898 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -577,21 +577,35 @@ schema definitions. === "Kotlin" The input and output schema is ADK's own `com.google.adk.kt.types.Schema`, - not the same-named type in the GenAI SDK. Since v0.8.0 it carries the JSON - Schema constraint fields, so a constraint can be declared instead of - described: `pattern`/`minLength`/`maxLength` for strings, - `minimum`/`maximum` for numbers, `minItems`/`maxItems` for arrays, plus - `format`, `nullable`, `default`, `anyOf` and `title`. + not the same-named type in the GenAI SDK. Since adk-kotlin 0.8.0 it carries + the JSON Schema constraint fields: `pattern`, `minLength`, `maxLength`, + `minimum`, `maximum`, `minItems`, `maxItems`, `format`, `nullable`, + `default`, `anyOf` and `title`. ```kotlin --8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example" ``` - Two of those fields carry conditions. Gemini rejects a schema whose `format` - is anything other than `int32`/`int64` on a number or `enum`/`date-time` on - a string. And `default` must hold a JSON-native value; serializing a - `Schema` that sets one needs a `Json` whose `serializersModule` has a - contextual serializer for `Any`, or it throws. + Four things are worth knowing before relying on this: + + - **The constraints are the model's to honour.** ADK forwards them to the + API but never checks them; its own validation covers structure only — + `type`, `required`, `nullable`, `anyOf` and `items`. + - **`outputKey` receives a `Map`, not text.** With `outputSchema` set, the + parsed object is what lands in state. If the response fails validation, + ADK logs the error and stores the raw string under the same key, so the + value's type is the only signal of which happened. + - **Only a top-level object schema is accepted**, as in the Java ADK. A + top-level array or primitive fails validation. + - **Tools are not excluded.** On models that support a response schema + alongside tools the schema is applied directly; on models that do not, + ADK falls back to a `set_model_response` tool. + + Two of the new fields carry conditions: Gemini rejects a `format` other than + `int32`/`int64` on `Type.INTEGER` or `Type.NUMBER`, or `enum`/`date-time` on + `Type.STRING`; and `default` must hold a JSON-native value, which ADK's own + `Json` serializes but a hand-rolled one without a contextual `Any` + serializer does not. ### Manage agent context diff --git a/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt b/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt index 153dce7024..5f859d4feb 100644 --- a/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt +++ b/examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt @@ -84,8 +84,6 @@ fun main() = // --8<-- [end:gen_config] // --8<-- [start:schema_example] - // Schema here is ADK's own com.google.adk.kt.types.Schema, not the - // same-named type in the GenAI SDK. val capitalOutput = Schema( type = Type.OBJECT, @@ -96,12 +94,18 @@ fun main() = Schema( type = Type.STRING, description = "The capital city of the country.", - // JSON Schema constraints, added in adk-kotlin 0.8.0. - minLength = 1, - pattern = "^[A-Z][A-Za-z .'-]*", + // Constraint fields, added in adk-kotlin 0.8.0. + minLength = 2, + maxLength = 60, + ), + "countryCode" to + Schema( + type = Type.STRING, + description = "ISO 3166-1 alpha-2 code for the country.", + pattern = "^[A-Z]{2}$", ), ), - required = listOf("capital"), + required = listOf("capital", "countryCode"), ) val structuredCapitalAgent = @@ -111,12 +115,11 @@ fun main() = instruction = Instruction( "You are a Capital Information Agent. Given a country, respond ONLY " + - "with a JSON object containing the capital. " + - """Format: {"capital": "capital_name"}""", + "with a JSON object holding the capital city and the country's " + + "ISO 3166-1 alpha-2 code.", ), outputSchema = capitalOutput, outputKey = "found_capital", - // Cannot use tools effectively here. ) // --8<-- [end:schema_example] From 2114b55f2de8d0698ae20cfdbf0d906409b0464e Mon Sep 17 00:00:00 2001 From: Shahin Saadati Date: Wed, 26 Aug 2026 09:20:44 -0700 Subject: [PATCH 3/5] Move the schema behaviour out of the Kotlin tab Checking the other SDKs showed most of what the Kotlin tab explained was not Kotlin's. Python, Java, Go and Kotlin all fall back to a set_model_response tool when a model cannot take a schema alongside tools, so the warning's advice to restructure into sub-agents was describing a limitation none of them has. All three of Python, Java and Kotlin store the parsed object under output_key once an output schema is set, so the bullet promising the text content was wrong before this change and wrong for every reader, not just Kotlin ones. What is left is a Java and Kotlin trait rather than a Kotlin one: both validate structure only and leave the constraint fields to the model, both accept only a top-level object, and both log and fall back to the raw string. Python enforces its Pydantic constraints and accepts list and primitive schemas, so a note naming the two JVM SDKs says it without implying Kotlin is the odd one out. Each claim now links the source it came from, pinned at v0.8.0. Drop the account of which format values Gemini accepts and link the Schema reference, which stays right on its own schedule. --- docs/agents/llm-agents.md | 63 ++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 4d093df898..d4f0168ab5 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -475,10 +475,10 @@ schema definitions. Using `output_schema` with `tools` in the same LLM request is only supported by specific models, including [Gemini 3.0](https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#structured-output). - For other models, workarounds using [function - tools](https://github.com/google/adk-python/blob/main/src/google/adk/flows/llm_flows/_output_schema_processor.py)) - in ADK may not work reliably. In such cases, consider using sub-agents that - handle output formatting separately. + For other models, ADK falls back to a [`set_model_response` function + tool](https://github.com/google/adk-python/blob/main/src/google/adk/flows/llm_flows/_output_schema_processor.py) + to collect the structured output, which may not work reliably. In such + cases, consider using sub-agents that handle output formatting separately. - **`output_key` (Optional):** Provide a string key. If set, the text content of the agent's *final* response will be automatically saved to the session's @@ -490,6 +490,27 @@ schema definitions. - In Golang, within a callback handler: `ctx.State().Set(output_key, agentResponseText)` + When `output_schema` is also set, the *parsed* response is stored instead of + the text: a `dict` in Python, and a `Map` in Java and Kotlin. + +!!! note "Schema validation in Java and Kotlin" + + Java and Kotlin check the response against the *structure* of the schema — + `type`, `required`, `nullable`, `anyOf` and `items` (see + [`SchemaUtils`](https://github.com/google/adk-kotlin/blob/v0.8.0/core/src/commonMain/kotlin/com/google/adk/kt/SchemaUtils.kt)). + Constraint fields such as `pattern`, `minLength` and `minimum` are sent to + the model as part of the schema, but ADK does not re-check them, so the + model decides whether to honor them. Python validates against a Pydantic + model, which does enforce the constraints declared on it. + + Java and Kotlin accept only a top-level object schema; a top-level array or + primitive fails validation. Python also supports list and primitive output + schemas. + + If the response fails validation, ADK logs the error and stores the raw + response string under `output_key` instead of the parsed object (see + [`LlmAgent`](https://github.com/google/adk-kotlin/blob/v0.8.0/core/src/commonMain/kotlin/com/google/adk/kt/agents/LlmAgent.kt)). + === "Python" The input and output schema is typically a `Pydantic` BaseModel. @@ -577,35 +598,21 @@ schema definitions. === "Kotlin" The input and output schema is ADK's own `com.google.adk.kt.types.Schema`, - not the same-named type in the GenAI SDK. Since adk-kotlin 0.8.0 it carries - the JSON Schema constraint fields: `pattern`, `minLength`, `maxLength`, - `minimum`, `maximum`, `minItems`, `maxItems`, `format`, `nullable`, - `default`, `anyOf` and `title`. + not the same-named type in the GenAI SDK. Starting with ADK Kotlin v0.8.0, + the JSON schema includes constraints for the following fields: `pattern`, + `minLength`, `maxLength`, `minimum`, `maximum`, `minItems`, `maxItems`, + `format`, `nullable`, `default`, `anyOf` and `title`. ```kotlin --8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example" ``` - Four things are worth knowing before relying on this: - - - **The constraints are the model's to honour.** ADK forwards them to the - API but never checks them; its own validation covers structure only — - `type`, `required`, `nullable`, `anyOf` and `items`. - - **`outputKey` receives a `Map`, not text.** With `outputSchema` set, the - parsed object is what lands in state. If the response fails validation, - ADK logs the error and stores the raw string under the same key, so the - value's type is the only signal of which happened. - - **Only a top-level object schema is accepted**, as in the Java ADK. A - top-level array or primitive fails validation. - - **Tools are not excluded.** On models that support a response schema - alongside tools the schema is applied directly; on models that do not, - ADK falls back to a `set_model_response` tool. - - Two of the new fields carry conditions: Gemini rejects a `format` other than - `int32`/`int64` on `Type.INTEGER` or `Type.NUMBER`, or `enum`/`date-time` on - `Type.STRING`; and `default` must hold a JSON-native value, which ADK's own - `Json` serializes but a hand-rolled one without a contextual `Any` - serializer does not. + `format` accepts only the values the model allows for the field's type. For + the accepted values, see the Gemini [`Schema` + reference](https://ai.google.dev/api/caching#Schema). + + `default` must hold a JSON-native value. ADK's own `Json` serializes one, + but a hand-rolled serializer without a contextual `Any` serializer does not. ### Manage agent context From 505bf784821043c9e5a8ccd17650cbb415cb1276 Mon Sep 17 00:00:00 2001 From: Joe Fernandez <931947+joefernandez@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:17:14 -0700 Subject: [PATCH 4/5] Apply suggestion from @joefernandez --- docs/agents/llm-agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index d4f0168ab5..b3e4f1272a 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -607,7 +607,7 @@ schema definitions. --8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example" ``` - `format` accepts only the values the model allows for the field's type. For + The `format` field accepts only the values the model allows for the field's type. For the accepted values, see the Gemini [`Schema` reference](https://ai.google.dev/api/caching#Schema). From 26bbe26c3cc3bd18e8519ac28d3445d3dfc15d03 Mon Sep 17 00:00:00 2001 From: Joe Fernandez <931947+joefernandez@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:17:27 -0700 Subject: [PATCH 5/5] Apply suggestion from @joefernandez --- docs/agents/llm-agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index b3e4f1272a..10cfed3034 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -611,7 +611,7 @@ schema definitions. the accepted values, see the Gemini [`Schema` reference](https://ai.google.dev/api/caching#Schema). - `default` must hold a JSON-native value. ADK's own `Json` serializes one, + The `default` field must contain a JSON-native value. ADK's own `Json` serializes one, but a hand-rolled serializer without a contextual `Any` serializer does not. ### Manage agent context