diff --git a/auto-configurations/common/spring-ai-autoconfigure-json-parser/pom.xml b/auto-configurations/common/spring-ai-autoconfigure-json-parser/pom.xml
new file mode 100644
index 00000000000..4b91727edda
--- /dev/null
+++ b/auto-configurations/common/spring-ai-autoconfigure-json-parser/pom.xml
@@ -0,0 +1,77 @@
+
+
+ * Provides customizable ObjectMappers for JSON parsing operations in tool calling, + * structured output, and model options handling. Users can override these beans to + * customize Jackson behavior. + * + * @author Daniel Albuquerque + */ +@AutoConfiguration +@ConditionalOnClass(ObjectMapper.class) +@EnableConfigurationProperties(JsonParserProperties.class) +public class JsonParserObjectMapperAutoConfiguration { + + /** + * Creates a configured ObjectMapper for JsonParser operations. + *
+ * This ObjectMapper is configured with: + *
{@code
+ * @Bean
+ * public ObjectMapper jsonParserObjectMapper() {
+ * return JsonMapper.builder()
+ * .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
+ * .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ * .build();
+ * }
+ * }
+ * @param properties the JsonParser configuration properties
+ * @return the configured ObjectMapper
+ */
+ @Bean(name = "jsonParserObjectMapper", defaultCandidate = false)
+ @ConditionalOnMissingBean(name = "jsonParserObjectMapper")
+ public ObjectMapper jsonParserObjectMapper(JsonParserProperties properties) {
+ JsonMapper.Builder builder = JsonMapper.builder()
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
+ .addModules(JacksonUtils.instantiateAvailableModules());
+
+ // Apply properties
+ if (properties.isAllowUnescapedControlChars()) {
+ builder.enable(com.fasterxml.jackson.core.json.JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS);
+ }
+
+ if (!properties.isWriteDatesAsTimestamps()) {
+ builder.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Creates a configured ObjectMapper for ModelOptionsUtils operations.
+ * + * This ObjectMapper is configured with: + *
{@code
+ * @Bean
+ * public ObjectMapper modelOptionsObjectMapper() {
+ * ObjectMapper mapper = JsonMapper.builder()
+ * .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ * .addModules(JacksonUtils.instantiateAvailableModules())
+ * .build()
+ * .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
+ *
+ * mapper.coercionConfigFor(Enum.class)
+ * .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
+ *
+ * return mapper;
+ * }
+ * }
+ * @param properties the configuration properties
+ * @return the configured ObjectMapper
+ */
+ @Bean(name = "modelOptionsObjectMapper", defaultCandidate = false)
+ @ConditionalOnMissingBean(name = "modelOptionsObjectMapper")
+ public ObjectMapper modelOptionsObjectMapper(JsonParserProperties properties) {
+ JsonMapper.Builder builder = JsonMapper.builder();
+
+ // Apply base configuration
+ if (properties.isFailOnUnknownProperties()) {
+ builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ }
+ else {
+ builder.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ }
+
+ if (properties.isFailOnEmptyBeans()) {
+ builder.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+ }
+ else {
+ builder.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+ }
+
+ builder.addModules(JacksonUtils.instantiateAvailableModules());
+
+ ObjectMapper mapper = builder.build();
+
+ // Configure empty string handling
+ if (properties.isAcceptEmptyStringAsNull()) {
+ mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
+ }
+
+ // Configure enum coercion (critical for API compatibility)
+ if (properties.isCoerceEmptyEnumStrings()) {
+ mapper.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
+ }
+
+ return mapper;
+ }
+
+ /**
+ * Creates a JsonParser bean configured with the custom ObjectMapper. This also sets
+ * the static configured mapper for backward compatibility with code using static
+ * methods.
+ * @param objectMapper the configured ObjectMapper
+ * @return the JsonParser instance
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public JsonParser jsonParser(@Qualifier("jsonParserObjectMapper") ObjectMapper objectMapper) {
+ // Set the static mapper for backward compatibility
+ JsonParser.setConfiguredObjectMapper(objectMapper);
+
+ // Create bean instance
+ return new JsonParser(objectMapper);
+ }
+
+ /**
+ * Initializes ModelOptionsUtils with the Spring-managed ObjectMapper. This setter
+ * allows ModelOptionsUtils static methods to use the Spring-configured mapper while
+ * maintaining backward compatibility.
+ * @param objectMapper the configured ObjectMapper for model options
+ */
+ @Bean
+ @ConditionalOnMissingBean(name = "modelOptionsUtilsInitializer")
+ public Object modelOptionsUtilsInitializer(@Qualifier("modelOptionsObjectMapper") ObjectMapper objectMapper) {
+ // Set the static mapper for backward compatibility
+ ModelOptionsUtils.setConfiguredObjectMapper(objectMapper);
+
+ // Return a marker object to satisfy bean contract
+ return new Object();
+ }
+
+}
diff --git a/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/java/org/springframework/ai/util/json/autoconfigure/JsonParserProperties.java b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/java/org/springframework/ai/util/json/autoconfigure/JsonParserProperties.java
new file mode 100644
index 00000000000..8ee7cfa122f
--- /dev/null
+++ b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/java/org/springframework/ai/util/json/autoconfigure/JsonParserProperties.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.util.json.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for JsonParser and ModelOptionsUtils ObjectMapper.
+ *
+ * @author Daniel Albuquerque
+ */
+@ConfigurationProperties(prefix = JsonParserProperties.CONFIG_PREFIX)
+public class JsonParserProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.json";
+
+ /**
+ * Allow unescaped control characters (like \n) in JSON strings. Useful when LLMs
+ * generate JSON with literal newlines.
+ */
+ private boolean allowUnescapedControlChars = false;
+
+ /**
+ * Write dates as ISO-8601 strings instead of timestamp arrays. When false (default),
+ * dates are written as strings like "2025-07-03".
+ */
+ private boolean writeDatesAsTimestamps = false;
+
+ /**
+ * Accept empty strings as null objects during deserialization. Used by
+ * ModelOptionsUtils for handling API responses.
+ */
+ private boolean acceptEmptyStringAsNull = true;
+
+ /**
+ * Coerce empty strings to null for enum types. Critical for handling API responses
+ * with empty finish_reason values.
+ */
+ private boolean coerceEmptyEnumStrings = true;
+
+ /**
+ * Fail on unknown properties during deserialization. When false (default), unknown
+ * properties are ignored.
+ */
+ private boolean failOnUnknownProperties = false;
+
+ /**
+ * Fail on empty beans during serialization. When false (default), empty beans are
+ * serialized as empty objects.
+ */
+ private boolean failOnEmptyBeans = false;
+
+ public boolean isAllowUnescapedControlChars() {
+ return this.allowUnescapedControlChars;
+ }
+
+ public void setAllowUnescapedControlChars(boolean allowUnescapedControlChars) {
+ this.allowUnescapedControlChars = allowUnescapedControlChars;
+ }
+
+ public boolean isWriteDatesAsTimestamps() {
+ return this.writeDatesAsTimestamps;
+ }
+
+ public void setWriteDatesAsTimestamps(boolean writeDatesAsTimestamps) {
+ this.writeDatesAsTimestamps = writeDatesAsTimestamps;
+ }
+
+ public boolean isAcceptEmptyStringAsNull() {
+ return this.acceptEmptyStringAsNull;
+ }
+
+ public void setAcceptEmptyStringAsNull(boolean acceptEmptyStringAsNull) {
+ this.acceptEmptyStringAsNull = acceptEmptyStringAsNull;
+ }
+
+ public boolean isCoerceEmptyEnumStrings() {
+ return this.coerceEmptyEnumStrings;
+ }
+
+ public void setCoerceEmptyEnumStrings(boolean coerceEmptyEnumStrings) {
+ this.coerceEmptyEnumStrings = coerceEmptyEnumStrings;
+ }
+
+ public boolean isFailOnUnknownProperties() {
+ return this.failOnUnknownProperties;
+ }
+
+ public void setFailOnUnknownProperties(boolean failOnUnknownProperties) {
+ this.failOnUnknownProperties = failOnUnknownProperties;
+ }
+
+ public boolean isFailOnEmptyBeans() {
+ return this.failOnEmptyBeans;
+ }
+
+ public void setFailOnEmptyBeans(boolean failOnEmptyBeans) {
+ this.failOnEmptyBeans = failOnEmptyBeans;
+ }
+
+}
diff --git a/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..794aa71557e
--- /dev/null
+++ b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.ai.util.json.autoconfigure.JsonParserObjectMapperAutoConfiguration
diff --git a/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserIntegrationTests.java b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserIntegrationTests.java
new file mode 100644
index 00000000000..f999b02ef56
--- /dev/null
+++ b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserIntegrationTests.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.util.json.autoconfigure;
+
+import java.time.LocalDate;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.util.json.JsonParser;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+/**
+ * Integration tests for {@link JsonParserObjectMapperAutoConfiguration} with tool calling
+ * scenarios.
+ *
+ * @author Daniel Albuquerque
+ */
+class JsonParserIntegrationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(JsonParserObjectMapperAutoConfiguration.class));
+
+ @Test
+ void shouldHandleUnescapedControlCharsInToolArguments() {
+ this.contextRunner.withPropertyValues("spring.ai.json.allow-unescaped-control-chars=true").run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+
+ // Simulate tool arguments with unescaped newlines (as might come from an
+ // LLM)
+ String jsonWithNewlines = """
+ {
+ "text": "Line 1
+ Line 2"
+ }
+ """;
+
+ // This should not throw JsonParseException
+ assertThatNoException().isThrownBy(() -> mapper.readValue(jsonWithNewlines, ToolRequest.class));
+
+ ToolRequest request = mapper.readValue(jsonWithNewlines, ToolRequest.class);
+ assertThat(request.text).contains("\n");
+ });
+ }
+
+ @Test
+ void shouldSerializeDatesAsStringsNotArrays() {
+ this.contextRunner.withPropertyValues("spring.ai.json.write-dates-as-timestamps=false").run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+
+ ToolRequestWithDate request = new ToolRequestWithDate();
+ request.date = LocalDate.of(2025, 7, 3);
+ request.text = "Test";
+
+ String json = mapper.writeValueAsString(request);
+
+ // Should be "2025-07-03" not [2025,7,3]
+ assertThat(json).doesNotContain("[2025");
+ assertThat(json).contains("2025-07-03");
+ });
+ }
+
+ @Test
+ void shouldUseConfiguredMapperInStaticJsonParserMethods() {
+ this.contextRunner.withPropertyValues("spring.ai.json.write-dates-as-timestamps=false").run(context -> {
+ // Force bean creation to set the configured mapper
+ context.getBean(JsonParser.class);
+
+ ToolRequestWithDate request = new ToolRequestWithDate();
+ request.date = LocalDate.of(2025, 7, 3);
+ request.text = "Test";
+
+ // Static method should use the Spring-configured mapper
+ String json = JsonParser.toJson(request);
+
+ assertThat(json).doesNotContain("[2025");
+ assertThat(json).contains("2025-07-03");
+ });
+ }
+
+ @Test
+ void shouldAllowCustomMapperOverride() {
+ this.contextRunner.withUserConfiguration(CustomMapperConfig.class).run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+
+ // Test unescaped control chars
+ String jsonWithNewlines = """
+ {
+ "text": "Line 1
+ Line 2"
+ }
+ """;
+
+ assertThatNoException().isThrownBy(() -> mapper.readValue(jsonWithNewlines, ToolRequest.class));
+
+ // Test date serialization
+ ToolRequestWithDate request = new ToolRequestWithDate();
+ request.date = LocalDate.of(2025, 7, 3);
+ request.text = "Test";
+
+ String json = mapper.writeValueAsString(request);
+ assertThat(json).doesNotContain("[2025");
+ assertThat(json).contains("2025-07-03");
+ });
+ }
+
+ @Test
+ void shouldWorkWithComplexToolArguments() throws JsonProcessingException {
+ this.contextRunner
+ .withPropertyValues("spring.ai.json.allow-unescaped-control-chars=true",
+ "spring.ai.json.write-dates-as-timestamps=false")
+ .run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+
+ String complexJson = """
+ {
+ "text": "Multi
+ line
+ text",
+ "date": "2025-07-03"
+ }
+ """;
+
+ ToolRequestWithDate request = mapper.readValue(complexJson, ToolRequestWithDate.class);
+ assertThat(request.text).contains("\n");
+ assertThat(request.date).isEqualTo(LocalDate.of(2025, 7, 3));
+
+ // Round-trip
+ String serialized = mapper.writeValueAsString(request);
+ ToolRequestWithDate deserialized = mapper.readValue(serialized, ToolRequestWithDate.class);
+ assertThat(deserialized.text).isEqualTo(request.text);
+ assertThat(deserialized.date).isEqualTo(request.date);
+ });
+ }
+
+ @Configuration
+ static class CustomMapperConfig {
+
+ @Bean(name = "jsonParserObjectMapper")
+ public ObjectMapper jsonParserObjectMapper() {
+ return JsonMapper.builder()
+ .enable(com.fasterxml.jackson.core.json.JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
+ .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .addModules(org.springframework.ai.util.JacksonUtils.instantiateAvailableModules())
+ .build();
+ }
+
+ }
+
+ static class ToolRequest {
+
+ public String text;
+
+ }
+
+ static class ToolRequestWithDate {
+
+ public String text;
+
+ public LocalDate date;
+
+ }
+
+}
diff --git a/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserObjectMapperAutoConfigurationTests.java b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserObjectMapperAutoConfigurationTests.java
new file mode 100644
index 00000000000..89f18478059
--- /dev/null
+++ b/auto-configurations/common/spring-ai-autoconfigure-json-parser/src/test/java/org/springframework/ai/util/json/autoconfigure/JsonParserObjectMapperAutoConfigurationTests.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.util.json.autoconfigure;
+
+import java.time.LocalDate;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.util.json.JsonParser;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link JsonParserObjectMapperAutoConfiguration}.
+ *
+ * @author Daniel Albuquerque
+ */
+class JsonParserObjectMapperAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(JsonParserObjectMapperAutoConfiguration.class));
+
+ @Test
+ void shouldAutoConfigureObjectMapper() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasBean("jsonParserObjectMapper");
+ assertThat(context).hasBean("modelOptionsObjectMapper");
+ ObjectMapper jsonParserMapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+ assertThat(jsonParserMapper).isNotNull();
+ });
+ }
+
+ @Test
+ void shouldAutoConfigureJsonParser() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(JsonParser.class);
+ JsonParser jsonParser = context.getBean(JsonParser.class);
+ assertThat(jsonParser).isNotNull();
+ });
+ }
+
+ @Test
+ void shouldAllowCustomObjectMapper() {
+ this.contextRunner.withUserConfiguration(CustomObjectMapperConfig.class).run(context -> {
+ assertThat(context).hasBean("jsonParserObjectMapper");
+ assertThat(context).hasBean("modelOptionsObjectMapper");
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+ assertThat(mapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse();
+ });
+ }
+
+ @Test
+ void shouldApplyPropertiesConfiguration() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.json.allow-unescaped-control-chars=true",
+ "spring.ai.json.write-dates-as-timestamps=false", "spring.ai.json.accept-empty-string-as-null=true",
+ "spring.ai.json.coerce-empty-enum-strings=true")
+ .run(context -> {
+ JsonParserProperties properties = context.getBean(JsonParserProperties.class);
+ assertThat(properties.isAllowUnescapedControlChars()).isTrue();
+ assertThat(properties.isWriteDatesAsTimestamps()).isFalse();
+ assertThat(properties.isAcceptEmptyStringAsNull()).isTrue();
+ assertThat(properties.isCoerceEmptyEnumStrings()).isTrue();
+ });
+ }
+
+ @Test
+ void shouldNotWriteDatesAsTimestampsWhenPropertyIsSet() {
+ this.contextRunner.withPropertyValues("spring.ai.json.write-dates-as-timestamps=false").run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+ assertThat(mapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse();
+
+ // Verify date serialization format
+ LocalDate date = LocalDate.of(2025, 7, 3);
+ String json = mapper.writeValueAsString(date);
+ // Should be "2025-07-03" not [2025,7,3]
+ assertThat(json).doesNotContain("[");
+ assertThat(json).contains("2025");
+ });
+ }
+
+ @Test
+ void shouldAllowUnescapedControlCharsWhenPropertyIsSet() {
+ this.contextRunner.withPropertyValues("spring.ai.json.allow-unescaped-control-chars=true").run(context -> {
+ ObjectMapper mapper = context.getBean("jsonParserObjectMapper", ObjectMapper.class);
+ // Note: ALLOW_UNESCAPED_CONTROL_CHARS is a JsonReadFeature, not easily
+ // testable via mapper.isEnabled()
+ // The actual test for this feature is in the integration tests
+ assertThat(mapper).isNotNull();
+ });
+ }
+
+ @Test
+ void shouldAutoConfigureModelOptionsObjectMapper() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasBean("modelOptionsObjectMapper");
+ ObjectMapper mapper = context.getBean("modelOptionsObjectMapper", ObjectMapper.class);
+ assertThat(mapper).isNotNull();
+ assertThat(mapper.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)).isTrue();
+ });
+ }
+
+ @Test
+ void shouldConfigureEnumCoercionForModelOptions() throws JsonProcessingException {
+ this.contextRunner.run(context -> {
+ ObjectMapper mapper = context.getBean("modelOptionsObjectMapper", ObjectMapper.class);
+
+ // Test enum coercion - empty string should be null
+ String jsonWithEmptyEnum = "{\"status\":\"\"}";
+ TestEnumClass result = mapper.readValue(jsonWithEmptyEnum, TestEnumClass.class);
+ assertThat(result.status).isNull();
+ });
+ }
+
+ @Test
+ void shouldUseConfiguredMapperInModelOptionsUtils() {
+ this.contextRunner.run(context -> {
+ // Trigger bean creation to initialize ModelOptionsUtils
+ context.getBean("modelOptionsUtilsInitializer");
+
+ // Test that ModelOptionsUtils uses the Spring-configured mapper
+ TestObject obj = new TestObject();
+ obj.name = "test";
+
+ String json = ModelOptionsUtils.toJsonString(obj);
+ assertThat(json).contains("test");
+
+ TestObject result = ModelOptionsUtils.jsonToObject(json, TestObject.class);
+ assertThat(result.name).isEqualTo("test");
+ });
+ }
+
+ @Test
+ void shouldApplyModelOptionsProperties() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.json.accept-empty-string-as-null=false",
+ "spring.ai.json.coerce-empty-enum-strings=false")
+ .run(context -> {
+ ObjectMapper mapper = context.getBean("modelOptionsObjectMapper", ObjectMapper.class);
+ assertThat(mapper.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)).isFalse();
+ });
+ }
+
+ @Configuration
+ static class CustomObjectMapperConfig {
+
+ @Bean(name = "jsonParserObjectMapper")
+ public ObjectMapper jsonParserObjectMapper() {
+ return com.fasterxml.jackson.databind.json.JsonMapper.builder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ }
+
+ }
+
+ static class TestObject {
+
+ public String name;
+
+ }
+
+ static class TestEnumClass {
+
+ public TestStatus status;
+
+ }
+
+ enum TestStatus {
+
+ ACTIVE, INACTIVE
+
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 281a1b3cce3..7842e715e43 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,6 +83,7 @@