Skip to content

Add the Kotlin tab for structured input and output schemas - #2151

Merged
happyhuman merged 9 commits into
mainfrom
docs-kotlin-schema-constraints
Aug 31, 2026
Merged

Add the Kotlin tab for structured input and output schemas#2151
happyhuman merged 9 commits into
mainfrom
docs-kotlin-schema-constraints

Conversation

@happyhuman

Copy link
Copy Markdown
Collaborator

Summary

The structured-data section of agents/llm-agents had Python, TypeScript, Go and
Java tabs but no Kotlin. This adds one, and adk-kotlin 0.8.0 is what makes it
worth writing: com.google.adk.kt.types.Schema gained the twelve JSON Schema
constraint fields — pattern, minLength, maxLength, minimum, maximum,
minItems, maxItems, format, nullable, default, anyOf and title. At
0.7.0 the class had six properties and none of these.

The snippet declares an output schema with minLength/maxLength on the capital
name and ^[A-Z]{2}$ on an ISO 3166-1 country code, then wires it into
LlmAgent(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:

  • The constraints are the model's to honour, not ADK's. SchemaUtils reads
    none of the twelve fields; its validation covers type, required,
    nullable, anyOf and items only. They are forwarded to the API. A reader
    would otherwise assume minLength guarantees a non-empty value in state.
  • outputKey receives a Map, not text. With outputSchema set,
    LlmAgent.kt:360-374 stores the parsed object; on a validation failure it
    logs and stores the raw string under the same key. So
    state["found_capital"] as String throws on the happy path, and the value's
    runtime type is the only signal of which outcome occurred — while the page
    bullet directly above the tab group says the text content is saved.
  • Only a top-level object schema is accepted, as in the Java ADK. Listing
    minItems/maxItems without this would point readers at a top-level array
    that fails validation and, per the previous point, fails silently.
  • Tools are not excluded. The warning above the group says output_schema
    with tools is only supported by specific models. Kotlin applies the schema
    directly on models that support both and falls back to a set_model_response
    tool on those that don't (LlmAgent.kt:100-107) — so a Kotlin reader should
    not 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 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.

Review notes

A sceptical pass over the first commit caught a bad example, fixed in 6c0242b4:
the original demonstrated pattern with ^[A-Z][A-Za-z .'-]* on a capital-city
name. 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 pattern moved 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 copied
from 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 are
inline, so inline here would break the page's convention and give up CI compile
coverage. It is a schema_example region instead.

Verification

  • ./tools/kotlin-snippets/runner.sh build …/CapitalAgent.ktPASS (JDK 17;
    the command CI runs). lintPASS.
  • verify_snippets.pyL0 symbols, L1 compile, L2 ktlint, L3 transclusions,
    L5 registration, L6 badge all PASS.
    L4 skips — no runSnippets task here.
  • Rendered the page with the extension list from mkdocs.yml: the target group
    comes back as ['Python', 'TypeScript', 'Go', 'Java', 'Kotlin'] and the new
    bullets render inside the Kotlin tab.

Every claim is grounded in the v0.8.0 tag of google/adk-kotlin — the version
examples/kotlin/build.gradle.kts pins — never the working tree, which is
currently 13 commits ahead of it.

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.
@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for adk-docs-preview ready!

Name Link
🔨 Latest commit 6c0242b
🔍 Latest deploy log https://app.netlify.com/projects/adk-docs-preview/deploys/6a861897fdaa4a00087daa43
😎 Deploy Preview https://deploy-preview-2151--adk-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@joefernandez joefernandez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some work. See comments.

Comment thread docs/agents/llm-agents.md
Comment thread docs/agents/llm-agents.md Outdated
Comment thread docs/agents/llm-agents.md Outdated
--8<-- "examples/kotlin/snippets/agents/llm-agent/CapitalAgent.kt:schema_example"
```

Four things are worth knowing before relying on this:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

Comment thread docs/agents/llm-agents.md Outdated
Comment on lines +591 to +597
- **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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread docs/agents/llm-agents.md Outdated
Comment on lines +604 to +608
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@happyhuman

Copy link
Copy Markdown
Collaborator Author

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:

Claim Python Java Kotlin Go
set_model_response fallback when tools + schema yes yes yes yes
output_key gets the parsed object, not text dict Map Map
Constraint fields not re-checked by ADK no (Pydantic enforces) yes yes
Top-level object schema only no (also list/primitive) yes yes
Raw string stored on validation failure no (raises) yes yes

So:

  • Tools are not excluded is ADK-wide. Python has _OutputSchemaRequestProcessor, Go has outputschema_processor.go, Java has OutputSchema.java. The warning's "consider using sub-agents" was pointing every reader around a limitation none of the SDKs has, so I named the automatic fallback in the warning and kept your reliability caveat.
  • output_key receives the parsed object is also ADK-wide, which means the existing bullet ("the text content ... will be automatically saved") was already wrong for anyone setting output_schema. Corrected in the bullet itself.
  • The remaining three are Java + Kotlin, not Kotlin — they're now one shared note titled "Schema validation in Java and Kotlin", which says what Python does differently instead of leaving Kotlin looking like the outlier.

On the type-sniffing bullet — you pushed hardest here and you were right to. But it isn't a Kotlin bug: LlmAgent.java:628-642 does exactly the same catch-log-store-raw. I've dropped "the value's type is the only signal of which happened" so nothing suggests sniffing the type, and just stated that the error is logged and the raw string stored. If that behavior should change, it's one issue against both JVM SDKs rather than adk-kotlin alone — happy to file it, but not from this PR.

Also done:

  • Version wording changed to yours verbatim.
  • Dropped "Four things are worth knowing before relying on this" and "ADK forwards them to the API but never checks them".
  • Claims now link source, pinned at v0.8.0 so the line references don't rot: SchemaUtils, LlmAgent. (The compaction page turned out not to link source — the precedent I followed is the adk-python link already in this section's warning.)
  • Gemini format value list replaced with a link to the Schema reference. Kept the default contextual-serializer note, split into its own sentence as you suggested.

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 ['Python', 'TypeScript', 'Go', 'Java', 'Kotlin'].

Comment thread docs/agents/llm-agents.md Outdated
Comment thread docs/agents/llm-agents.md Outdated

@joefernandez joefernandez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updates! Approved.

@happyhuman
happyhuman merged commit 3e9b83d into main Aug 31, 2026
11 checks passed
@happyhuman
happyhuman deleted the docs-kotlin-schema-constraints branch August 31, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants