Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ public Map<String, ToolMetadata> createToolRegistry(Map<String, BaseTool> tools)
/**
* Converts ADK Schema to Spring AI compatible parameter schema.
*
* <p>This provides basic schema conversion for tool parameters.
* <p>Maps the {@code type}, {@code description}, {@code properties}, {@code required}, {@code
* enum} and {@code items} keywords ({@code properties} values and {@code items} recursively).
* Other JSON-Schema keywords (e.g. {@code nullable}, {@code anyOf}, {@code format}) are not
* carried over yet.
*
* @param schema The ADK schema to convert
* @return A Map representing the Spring AI compatible schema
Expand All @@ -97,6 +100,15 @@ public Map<String, Object> convertSchemaToSpringAi(Schema schema) {

schema.required().ifPresent(required -> springAiSchema.put("required", required));

// Preserve enum constraints and array item schemas; skip empty enums (unsatisfiable).
schema
.enum_()
.filter(values -> !values.isEmpty())
.ifPresent(values -> springAiSchema.put("enum", values));
schema
.items()
.ifPresent(itemSchema -> springAiSchema.put("items", convertSchemaToSpringAi(itemSchema)));

return springAiSchema;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.adk.tools.BaseTool;
import com.google.adk.tools.ToolContext;
import com.google.genai.types.FunctionDeclaration;
Expand Down Expand Up @@ -158,6 +160,143 @@ void testConvertSchemaToSpringAiWithObjectType() {
assertThat(converted).containsEntry("required", List.of("name"));
}

@Test
void testConvertSchemaToSpringAiPreservesEnumAndItems() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

// Regression: convertSchemaToSpringAi used to silently drop "enum" and "items",
// degrading enum parameters to free-form text and losing array element schemas.
Schema enumParam =
Schema.builder()
.type("STRING")
.description("Report detail level")
.enum_("basic", "detailed")
.build();
Schema itemsSchema = Schema.builder().type("STRING").enum_("cardio", "dental").build();
Schema arrayParam =
Schema.builder().type("ARRAY").description("Included services").items(itemsSchema).build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — added testConvertSchemaToSpringAiRecursesIntoObjectItemsProperties covering array-of-objects items with their own properties and required.

Schema objectSchema =
Schema.builder()
.type("OBJECT")
.properties(
Map.of(
"level", enumParam,
"services", arrayParam))
.required(List.of("level"))
.build();

Map<String, Object> converted = toolConverter.convertSchemaToSpringAi(objectSchema);

Map<String, Object> properties = asMap(converted.get("properties"));

Map<String, Object> convertedEnum = asMap(properties.get("level"));
assertThat(convertedEnum).containsEntry("type", "string");
assertThat(convertedEnum).containsEntry("enum", List.of("basic", "detailed"));

Map<String, Object> convertedArray = asMap(properties.get("services"));
assertThat(convertedArray).containsEntry("type", "array");
Map<String, Object> convertedItems = asMap(convertedArray.get("items"));
assertThat(convertedItems).containsEntry("type", "string");
assertThat(convertedItems).containsEntry("enum", List.of("cardio", "dental"));
}

@Test
void testConvertSchemaToSpringAiRecursesIntoObjectItemsProperties() {
// Array-of-objects parameters (the shape FunctionTool generates for List<>) must keep
// the properties of each item: recursion has to flow through properties inside items.
Schema itemObject =
Schema.builder()
.type("OBJECT")
.properties(
Map.of(
"name", Schema.builder().type("STRING").build(),
"level", Schema.builder().type("STRING").enum_("basic", "detailed").build()))
.required(List.of("name"))
.build();
Schema arrayOfObjects =
Schema.builder().type("ARRAY").description("Line items").items(itemObject).build();

Map<String, Object> converted = toolConverter.convertSchemaToSpringAi(arrayOfObjects);

assertThat(converted).containsEntry("type", "array");
Map<String, Object> items = asMap(converted.get("items"));
assertThat(items).containsEntry("type", "object");
Map<String, Object> itemProperties = asMap(items.get("properties"));
Map<String, Object> itemEnum = asMap(itemProperties.get("level"));
assertThat(itemEnum).containsEntry("enum", List.of("basic", "detailed"));
assertThat(items).containsEntry("required", List.of("name"));
}

@Test
void testConvertToSpringAiToolsOutputSchemaContainsEnumAndItems() throws Exception {
// End-to-end over the user-visible output (the repro from the issue): the serialized
// inputSchema JSON of a converted tool must contain enum and items.
FunctionDeclaration declaration =
FunctionDeclaration.builder()
.name("reportTool")
.description("Generates a report")
.parameters(
Schema.builder()
.type("OBJECT")
.properties(
Map.of(
"level",
Schema.builder().type("STRING").enum_("basic", "detailed").build(),
"services",
Schema.builder()
.type("ARRAY")
.items(
Schema.builder()
.type("STRING")
.enum_("cardio", "dental")
.build())
.build()))
.required(List.of("level"))
.build())
.build();
BaseTool tool =
new BaseTool("reportTool", "Generates a report") {
@Override
public Optional<FunctionDeclaration> declaration() {
return Optional.of(declaration);
}

@Override
public Single<Map<String, Object>> runAsync(
Map<String, Object> args, ToolContext toolContext) {
return Single.just(Map.of());
}
};

List<ToolCallback> callbacks = toolConverter.convertToSpringAiTools(Map.of("reportTool", tool));

assertThat(callbacks).hasSize(1);
String inputSchema = callbacks.get(0).getToolDefinition().inputSchema();
Map<String, Object> schemaJson =
new ObjectMapper().readValue(inputSchema, new TypeReference<Map<String, Object>>() {});
Map<String, Object> properties = asMap(schemaJson.get("properties"));
assertThat(asMap(properties.get("level"))).containsEntry("enum", List.of("basic", "detailed"));
Map<String, Object> services = asMap(properties.get("services"));
assertThat(services).containsEntry("type", "array");
assertThat(asMap(services.get("items"))).containsEntry("enum", List.of("cardio", "dental"));
}

@Test
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_(List.of()).build();

Map<String, Object> converted = toolConverter.convertSchemaToSpringAi(emptyEnumSchema);

assertThat(converted).containsEntry("type", "string");
assertThat(converted).doesNotContainKey("enum");
}

@SuppressWarnings("unchecked")
private static Map<String, Object> asMap(Object value) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Suggested change
private static Map<String, Object> asMap(Object value) {
@SuppressWarnings("unchecked")
private static Map<String, Object> asMap(Object value) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — suppressed; the cast is guarded by the isInstanceOf assertion.

assertThat(value).isInstanceOf(Map.class);
return (Map<String, Object>) value;
}

@Test
void testCreateToolRegistryWithToolWithoutDeclaration() {
BaseTool testTool =
Expand Down
Loading