Repository navigation
Conversation
wikaaaaa
left a comment
There was a problem hiding this comment.
Thanks for the fix! The change looks correct and safe.
I left a few small comments inline, mostly about tests and comment length. One more point that doesn't fit on a diff line:
Javadoc: convertSchemaToSpringAi is still described as "basic schema conversion". Could you list the keywords it maps (type, description, properties, required, enum, items)? Then it's clear that keywords like nullable, anyOf and format aren't carried over yet.
On option 2: agreed that this PR is the right first step, and a follow-up would be welcome. Note that serializing the genai Schema verbatim won't give valid JSON Schema by itself:
- types come out uppercase ("STRING")
- nullable needs translating to "type": [..., "null"]
- anyOf needs to be converted recursively
- Gemini-specific keys like propertyOrdering may be rejected by stricter providers
So it would be "serialize everything, then normalize".
| // Preserve enum constraints and array element schemas: silently dropping them degrades | ||
| // enum parameters to free-form text (the model invents values) and loses the element | ||
| // schema of array parameters. | ||
| // An empty enum list would serialize to "enum": [] - an unsatisfiable constraint under | ||
| // JSON Schema semantics; skip it instead of passing through a value that makes every | ||
| // call to this tool invalid. |
There was a problem hiding this comment.
Could we keep this to a single line? The reasoning is great but fits better in the commit message / PR description. Something like:
| // Preserve enum constraints and array element schemas: silently dropping them degrades | |
| // enum parameters to free-form text (the model invents values) and loses the element | |
| // schema of array parameters. | |
| // An empty enum list would serialize to "enum": [] - an unsatisfiable constraint under | |
| // JSON Schema semantics; skip it instead of passing through a value that makes every | |
| // call to this tool invalid. | |
| // Preserve enum constraints and array item schemas; skip empty enums (unsatisfiable). |
There was a problem hiding this comment.
Done — adopted your suggestion verbatim.
| } | ||
|
|
||
| @Test | ||
| void testConvertSchemaToSpringAiPreservesEnumAndItems() { |
There was a problem hiding this comment.
These tests exercise convertSchemaToSpringAi directly, but the bug as reported is in what ends up in convertToSpringAiTools(...).get(0).getToolDefinition().inputSchema(). Could you add one test that goes through convertToSpringAiTools and asserts the serialized inputSchema JSON contains enum and items (essentially the repro from the issue)? That guards the user-visible output, including the JSON serialization step.
There was a problem hiding this comment.
Done — added testConvertToSpringAiToolsOutputSchemaContainsEnumAndItems: it converts a tool end-to-end and asserts the serialized inputSchema JSON contains enum and items (the repro from the issue, as a regression test).
| .build(); | ||
| Schema itemsSchema = Schema.builder().type("STRING").enum_("cardio", "dental").build(); | ||
| Schema arrayParam = | ||
| Schema.builder().type("ARRAY").description("Included services").items(itemsSchema).build(); |
There was a problem hiding this comment.
Nice that the nested item enum is covered. Could you also add a case where items is an OBJECT schema with its own properties (array of objects)? That proves the recursion also flows through properties inside items, which is the shape FunctionTool generates for List<>.
There was a problem hiding this comment.
Done — added testConvertSchemaToSpringAiRecursesIntoObjectItemsProperties covering array-of-objects items with their own properties and required.
|
|
||
| Map<String, Object> converted = toolConverter.convertSchemaToSpringAi(objectSchema); | ||
|
|
||
| Map<String, Object> properties = (Map<String, Object>) converted.get("properties"); |
There was a problem hiding this comment.
These (Map<String, Object>) casts produce unchecked-cast warnings. Could you add @SuppressWarnings("unchecked") on the test method (or extract a small typed helper, e.g. private static Map<String, Object> asMap(Object o))?
There was a problem hiding this comment.
Done — extracted a small asMap helper. Its cast is guarded by the instanceof assertion, with the suppression added per your second-round comment.
| void testConvertSchemaToSpringAiSkipsEmptyEnum() { | ||
| // "enum": [] is unsatisfiable under JSON Schema semantics (no valid value exists); | ||
| // an empty enum must be skipped rather than forwarded. | ||
| Schema emptyEnumSchema = Schema.builder().type("STRING").enum_(new String[0]).build(); |
There was a problem hiding this comment.
Nit: List.of() reads a bit more clearly than an empty varargs array.
| Schema emptyEnumSchema = Schema.builder().type("STRING").enum_(new String[0]).build(); | |
| Schema emptyEnumSchema = Schema.builder().type("STRING").enum_(List.of()).build(); |
a56645f to
b5eb358
Compare
|
Hi @caps-xia, Thank you for contributing and taking the time to open this PR.Our team is currently reviewing your changes and we will reach out if we need any further information.Thank you. |
| public io.reactivex.rxjava3.core.Single<Map<String, Object>> runAsync( | ||
| Map<String, Object> args, ToolContext toolContext) { | ||
| return io.reactivex.rxjava3.core.Single.just(Map.of()); |
There was a problem hiding this comment.
Nit: Single is already imported at the top of the file, so the fully qualified name isn't needed.
| public io.reactivex.rxjava3.core.Single<Map<String, Object>> runAsync( | |
| Map<String, Object> args, ToolContext toolContext) { | |
| return io.reactivex.rxjava3.core.Single.just(Map.of()); | |
| public Single<Map<String, Object>> runAsync( | |
| Map<String, Object> args, ToolContext toolContext) { | |
| return Single.just(Map.of()); |
There was a problem hiding this comment.
Done — using the existing import.
| Map<String, Object> schemaJson = | ||
| new com.fasterxml.jackson.databind.ObjectMapper().readValue(inputSchema, Map.class); |
There was a problem hiding this comment.
Nit: could you import ObjectMapper instead of using the fully qualified name? Using a TypeReference also avoids the raw Map.class unchecked conversion:
| Map<String, Object> schemaJson = | |
| new com.fasterxml.jackson.databind.ObjectMapper().readValue(inputSchema, Map.class); | |
| Map<String, Object> schemaJson = | |
| new ObjectMapper().readValue(inputSchema, new TypeReference<Map<String, Object>>() {}); |
(needs import com.fasterxml.jackson.core.type.TypeReference; and import com.fasterxml.jackson.databind.ObjectMapper;)
There was a problem hiding this comment.
Done — imported ObjectMapper and switched to a TypeReference to avoid the raw Map.class conversion.
| assertThat(converted).doesNotContainKey("enum"); | ||
| } | ||
|
|
||
| private static Map<String, Object> asMap(Object value) { |
There was a problem hiding this comment.
Thanks for the helper! The cast inside still raises an unchecked warning; since it's guarded by the isInstanceOf assertion, it's safe to suppress here:
| private static Map<String, Object> asMap(Object value) { | |
| @SuppressWarnings("unchecked") | |
| private static Map<String, Object> asMap(Object value) { |
There was a problem hiding this comment.
Done — suppressed; the cast is guarded by the isInstanceOf assertion.
convertSchemaToSpringAi mapped only type/properties/description/required, silently dropping enum constraints and array item schemas. Enum parameters degraded to free-form text (the model invents values) and array parameters lost their element schema. Map both, recursing into items so nested enums and object arrays survive conversion. Fixes google#1609
b5eb358 to
7b1a5b8
Compare
|
Apologies for the silence on the threads — the fixes went in with each push. Everything is addressed in 7b1a5b8 (single commit): round-one comments in b5eb358 (javadoc now lists the mapped keywords, condensed comment, end-to-end test through convertToSpringAiTools, array-of-objects items case, asMap helper, List.of()), round-two nits applied verbatim (imports + suppressed cast). Full module suite green locally (219 tests). Good point on option 2 — happy to send the serialize-the-original-schema pass as a follow-up PR once this lands. |
Link to Issue or Description of Change
Closes: #1609
Problem:
ToolConverter#convertSchemaToSpringAimaps onlytype,properties,descriptionandrequiredwhen converting a genaiSchemainto the Spring AI JSON-schema map.enumanditemsare silently dropped:itemsare lost as well.The drop is silent: the generated schema still looks complete (names/types/descriptions present), which reliably misleads debugging toward prompts or model capability instead of the conversion layer.
Solution:
Map
enumanditemsalongsiderequired, recursing intoitemsso nested schemas survive conversion — the same treatmentpropertiesalready gets. Minimal and additive:enumlists are skipped, since"enum": []is an unsatisfiable constraint under JSON Schema semantics;type/description/properties/required) is touched.Testing Plan
Unit Tests:
testConvertSchemaToSpringAiPreservesEnumAndItemstoToolConverterTest: an object schema with an enum parameter and an array parameter whose items carry their own enum; asserts both keywords (and the nested item enum) survive conversion whiletype/requiredbehavior stays unchanged.testConvertSchemaToSpringAiSkipsEmptyEnum: an emptyenumlist is not forwarded ("enum": []would make every call to the tool invalid).mvn -pl contrib/spring-ai -am test→ 217 tests, 0 failures, BUILD SUCCESS.Manual End-to-End (E2E) Tests:
Reproduced with a real OpenAI-compatible gateway and a tool declaring
enum/items:enum/itemsand invented parameter values (behavior verified identical on 1.9.0 and 1.11.0 — probe output attached in the issue).enum/items, reaches the model unchanged. The same pass-through path has been running in a production fork for months via a tool-callback restore workaround, so downstream consumption of these keywords is verified in production.Checklist
Additional context
This follows option 1 from my comment on the issue (small and reviewable). Serializing the original schema wholesale (option 2) can be a follow-up if maintainers prefer it.