Add the Kotlin tab for structured input and output schemas - #2151
Conversation
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].
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.
✅ Deploy Preview for adk-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
joefernandez
left a comment
There was a problem hiding this comment.
Needs some work. See comments.
| --8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example" | ||
| ``` | ||
|
|
||
| Four things are worth knowing before relying on this: |
There was a problem hiding this comment.
"Four things are worth knowing before relying on this" sets a hedging frame — it tells the reader the feature is something to be wary of before they've seen a reason to be.
At the minimum, remove or restate this over-the-top language:
- "ADK forwards them to the API but never checks them"
If these four points are correct they're just how the feature works, and should read that way: "How schema validation behaves" or similar, stated as documentation rather than as warnings. The counted-list framing ("four things") also becomes a maintenance trap the moment someone adds a fifth.
| - **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. |
There was a problem hiding this comment.
Two substantive behavioral claims that need sourcing, and one that describes a rough edge as if it were an API.
"ADK forwards them to the API but never checks them" and the outputKey fallback are real, useful things to document — but they're stated flatly with nothing to verify against. Can you link the validation code, the way the compaction page links LlmEventSummarizer? Otherwise a reader has no way to confirm it and no way to notice when it changes.
The second bullet is the one I'd push back on hardest: "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." That's telling readers to type-sniff a state value to find out whether an error occurred. If that really is the only signal, it's a bug worth filing against adk-kotlin rather than a technique worth documenting, writing it down here makes it more likely someone will build on it.
Is this behavior Kotlin-only, or does Python/Java do the same? If it's shared, it belongs above the tabs (see my first comment).
| 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. |
There was a problem hiding this comment.
Third-party API behavior that will drift out from under this page.
"Gemini rejects a format other than int32/int64 on Type.INTEGER or Type.NUMBER, or enum/date-time on Type.STRING" is a claim about the Gemini API's current accepted values, not about ADK. That list changes on Gemini's release schedule, with nothing tying it to this repo — no test covers it, no CI catches it, and nobody updates adk-docs when Gemini adds a format. It'll be quietly wrong at some point and no one will know.
Prefer linking the Gemini structured-output / Schema reference and letting it be authoritative, rather than restating a value list here.
The default half is different — the contextual Any serializer requirement is genuinely ADK-Kotlin-specific and worth keeping. Might be clearer split into its own sentence rather than sharing one with the Gemini constraint.
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.
|
Thanks — this was the right call, and checking the other SDKs made it clearer than I expected. Addressed in 2114b55. You were right that most of it wasn't Kotlin-specific. I checked all four SDKs; only one of the four bullets survived as Kotlin's:
So:
On the type-sniffing bullet — you pushed hardest here and you were right to. But it isn't a Kotlin bug: Also done:
The Kotlin tab is now a lead-in, the snippet, and two short sentences — same shape as its siblings. Verification: L0, L1 compile, L2 lint, L3, L5, L6 pass; L4 skipped as before. Tab group still renders |
joefernandez
left a comment
There was a problem hiding this comment.
Thanks for updates! Approved.
Summary
The structured-data section of
agents/llm-agentshad Python, TypeScript, Go andJava tabs but no Kotlin. This adds one, and adk-kotlin 0.8.0 is what makes it
worth writing:
com.google.adk.kt.types.Schemagained the twelve JSON Schemaconstraint fields —
pattern,minLength,maxLength,minimum,maximum,minItems,maxItems,format,nullable,default,anyOfandtitle. At0.7.0 the class had six properties and none of these.
The snippet declares an output schema with
minLength/maxLengthon the capitalname and
^[A-Z]{2}$on an ISO 3166-1 country code, then wires it intoLlmAgent(outputSchema = …, outputKey = …).Four corrections to what this page implies for Kotlin
Most of the review effort went here rather than into the snippet. The page's
surrounding bullets and warning describe Python behaviour, and a bare Kotlin tab
would have inherited all of it. Each of these is checked against the pinned
sources, not inferred:
SchemaUtilsreadsnone of the twelve fields; its validation covers
type,required,nullable,anyOfanditemsonly. They are forwarded to the API. A readerwould otherwise assume
minLengthguarantees a non-empty value in state.outputKeyreceives aMap, not text. WithoutputSchemaset,LlmAgent.kt:360-374stores the parsed object; on a validation failure itlogs and stores the raw string under the same key. So
state["found_capital"] as Stringthrows on the happy path, and the value'sruntime type is the only signal of which outcome occurred — while the page
bullet directly above the tab group says the text content is saved.
minItems/maxItemswithout this would point readers at a top-level arraythat fails validation and, per the previous point, fails silently.
output_schemawith tools is only supported by specific models. Kotlin applies the schema
directly on models that support both and falls back to a
set_model_responsetool on those that don't (
LlmAgent.kt:100-107) — so a Kotlin reader shouldnot restructure into sub-agents to dodge a limitation they don't have.
Two of the new fields carry conditions of their own, both from the upstream
KDoc: Gemini rejects a
formatother thanint32/int64onType.INTEGERorType.NUMBER, orenum/date-timeonType.STRING; anddefaultmust hold aJSON-native value, which ADK's own
Jsonserializes but a hand-rolled onewithout a contextual
Anyserializer does not.Review notes
A sceptical pass over the first commit caught a bad example, fixed in
6c0242b4:the original demonstrated
patternwith^[A-Z][A-Za-z .'-]*on a capital-cityname. Under full-match semantics that rejects Bogotá, Brasília, Reykjavík, San
José and "Washington, D.C." — correct answers the model would be penalised for.
Under JSON Schema's partial-match default it rejects nothing at all, since the
tail can match zero characters. Either way it was the wrong constraint to teach
for that data, so
patternmoved to the country code, where it is exactly right,and the instruction was updated to match the schema it is paired with.
The same pass removed a
// Cannot use tools effectively here.comment copiedfrom the Python and Java tabs, which is false for adk-kotlin.
Not inline, unlike the backlog row
KT-22 specifies inline Kotlin. Every other Kotlin tab on this page transcludes a
region from
CapitalAgent.kt, even though its Python and Java siblings areinline, so inline here would break the page's convention and give up CI compile
coverage. It is a
schema_exampleregion instead.Verification
./tools/kotlin-snippets/runner.sh build …/CapitalAgent.kt→ PASS (JDK 17;the command CI runs).
lint→ PASS.verify_snippets.py→ L0 symbols, L1 compile, L2 ktlint, L3 transclusions,L5 registration, L6 badge all PASS. L4 skips — no
runSnippetstask here.mkdocs.yml: the target groupcomes back as
['Python', 'TypeScript', 'Go', 'Java', 'Kotlin']and the newbullets render inside the Kotlin tab.
Every claim is grounded in the
v0.8.0tag ofgoogle/adk-kotlin— the versionexamples/kotlin/build.gradle.ktspins — never the working tree, which iscurrently 13 commits ahead of it.