diff --git a/spring-ai-model/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java b/spring-ai-model/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java index 64b64a77a78..176780ebe51 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java @@ -37,7 +37,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.model.KotlinModule; import org.springframework.ai.util.JacksonUtils; +import org.springframework.core.KotlinDetector; import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.NonNull; @@ -136,6 +138,11 @@ private void generateSchema() { com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON) .with(jacksonModule) .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT); + + if (KotlinDetector.isKotlinReflectPresent()) { + configBuilder.with(new KotlinModule()); + } + SchemaGeneratorConfig config = configBuilder.build(); SchemaGenerator generator = new SchemaGenerator(config); JsonNode jsonNode = generator.generateSchema(this.type); diff --git a/spring-ai-model/src/test/kotlin/org/springframework/ai/converter/BeanOutputConverterTests.kt b/spring-ai-model/src/test/kotlin/org/springframework/ai/converter/BeanOutputConverterTests.kt new file mode 100644 index 00000000000..1bea428d430 --- /dev/null +++ b/spring-ai-model/src/test/kotlin/org/springframework/ai/converter/BeanOutputConverterTests.kt @@ -0,0 +1,58 @@ +package org.springframework.ai.converter + +import com.fasterxml.jackson.databind.ObjectMapper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class KotlinBeanOutputConverterTests { + + private data class Foo(val bar: String, val baz: String?) + private data class FooWithDefault(val bar: String, val baz: Int = 10) + + private val objectMapper = ObjectMapper() + + @Test + fun `test Kotlin data class schema generation using getJsonSchema`() { + val converter = BeanOutputConverter(Foo::class.java) + + val schemaJson = converter.jsonSchema + + val schemaNode = objectMapper.readTree(schemaJson) + + val required = schemaNode["required"] + assertThat(required).isNotNull + assertThat(required.toString()).contains("bar") + assertThat(required.toString()).doesNotContain("baz") + + val properties = schemaNode["properties"] + assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + + val bazTypeNode = properties["baz"]["type"] + if (bazTypeNode.isArray) { + assertThat(bazTypeNode.toString()).contains("string") + assertThat(bazTypeNode.toString()).contains("null") + } else { + assertThat(bazTypeNode.asText()).isEqualTo("string") + } + } + + @Test + fun `test Kotlin data class with default values`() { + val converter = BeanOutputConverter(FooWithDefault::class.java) + + val schemaJson = converter.jsonSchema + + val schemaNode = objectMapper.readTree(schemaJson) + + val required = schemaNode["required"] + assertThat(required).isNotNull + assertThat(required.toString()).contains("bar") + assertThat(required.toString()).doesNotContain("baz") + + val properties = schemaNode["properties"] + assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + + val bazTypeNode = properties["baz"]["type"] + assertThat(bazTypeNode.asText()).isEqualTo("integer") + } +}