Skip to content

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

Open
happyhuman wants to merge 2 commits into
mainfrom
docs-kotlin-schema-constraints
Open

Add the Kotlin tab for structured input and output schemas#2151
happyhuman wants to merge 2 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.

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.

1 participant