diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java index f3c5c1b418..ef08bb60ec 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java @@ -20,11 +20,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties; @@ -79,7 +79,7 @@ public List stdioTransports(McpStdioClientProperties st for (Map.Entry serverParameters : stdioProperties.toServerParameters().entrySet()) { var transport = new StdioClientTransport(serverParameters.getValue(), - new JacksonMcpJsonMapper(new ObjectMapper())); + new JacksonMcpJsonMapper(JsonMapper.shared())); stdioTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java index 80c0ab12bc..320003bc96 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java @@ -24,10 +24,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.ServerParameters; import org.jspecify.annotations.Nullable; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; @@ -80,7 +80,7 @@ private Map resourceToServerParameters() { return Collections.emptyMap(); } try { - Map> stdioConnection = new ObjectMapper() + Map> stdioConnection = JsonMapper.shared() .readValue(this.serversConfiguration.getInputStream(), new TypeReference<>() { }); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java index 2b15232a62..85a1664f2e 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java @@ -21,13 +21,13 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpSseClientConnectionDetails; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; @@ -56,7 +56,7 @@ * Key features: *
    *
  • Creates HTTP client-based SSE transports for configured MCP server connections - *
  • Configures ObjectMapper for JSON serialization/deserialization + *
  • Configures JsonMapper for JSON serialization/deserialization *
  • Supports multiple named server connections with different URLs *
* @@ -85,12 +85,12 @@ PropertiesMcpSseClientConnectionDetails mcpSseClientConnectionDetails(McpSseClie *
    *
  • A new HttpClient instance *
  • Server URL from properties - *
  • ObjectMapper for JSON processing + *
  • JsonMapper for JSON processing *
  • A sync or async HTTP request customizer. Sync takes precedence. *
* @param connectionDetails the SSE client connection details containing server * configurations - * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * @param jsonMapperProvider the provider for JsonMapper or a new instance if not * available * @param syncHttpRequestCustomizer provider for * {@link McpSyncHttpClientRequestCustomizer} if available @@ -100,11 +100,11 @@ PropertiesMcpSseClientConnectionDetails mcpSseClientConnectionDetails(McpSseClie */ @Bean public List sseHttpClientTransports(McpSseClientConnectionDetails connectionDetails, - ObjectProvider objectMapperProvider, + ObjectProvider jsonMapperProvider, ObjectProvider syncHttpRequestCustomizer, ObjectProvider asyncHttpRequestCustomizer) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable(JsonMapper::new); List sseTransports = new ArrayList<>(); @@ -123,7 +123,7 @@ public List sseHttpClientTransports(McpSseClientConnect var transportBuilder = HttpClientSseClientTransport.builder(baseUrl) .sseEndpoint(sseEndpoint) .clientBuilder(HttpClient.newBuilder()) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)); + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)); asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer); syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java index 3f1afef25d..544f0577ff 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java @@ -21,13 +21,13 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; @@ -55,7 +55,7 @@ *
    *
  • Creates HTTP client-based Streamable HTTP transports for configured MCP server * connections - *
  • Configures ObjectMapper for JSON serialization/deserialization + *
  • Configures JsonMapper for JSON serialization/deserialization *
  • Supports multiple named server connections with different URLs *
  • Adds a sync or async HTTP request customizer. Sync takes precedence. *
@@ -81,11 +81,11 @@ public class StreamableHttpHttpClientTransportAutoConfiguration { *
    *
  • A new HttpClient instance *
  • Server URL from properties - *
  • ObjectMapper for JSON processing + *
  • JsonMapper for JSON processing *
* @param streamableProperties the Streamable HTTP client properties containing server * configurations - * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * @param jsonMapperProvider the provider for JsonMapper or a new instance if not * available * @param syncHttpRequestCustomizer provider for * {@link McpSyncHttpClientRequestCustomizer} if available @@ -95,11 +95,11 @@ public class StreamableHttpHttpClientTransportAutoConfiguration { */ @Bean public List streamableHttpHttpClientTransports( - McpStreamableHttpClientProperties streamableProperties, ObjectProvider objectMapperProvider, + McpStreamableHttpClientProperties streamableProperties, ObjectProvider jsonMapperProvider, ObjectProvider syncHttpRequestCustomizer, ObjectProvider asyncHttpRequestCustomizer) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable(JsonMapper::shared); List streamableHttpTransports = new ArrayList<>(); @@ -114,7 +114,7 @@ public List streamableHttpHttpClientTransports( .builder(baseUrl) .endpoint(streamableHttpEndpoint) .clientBuilder(HttpClient.newBuilder()) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)); + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)); asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer); syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java index a8499a97d1..722b04f9af 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java @@ -19,9 +19,9 @@ import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration; @@ -104,11 +104,11 @@ void customSseEndpointIsRespected() { } @Test - void customObjectMapperIsUsed() { - this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) + void customJsonMapperIsUsed() { + this.applicationContext.withUserConfiguration(CustomJsonMapperConfiguration.class) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + assertThat(context.getBean(JsonMapper.class)).isNotNull(); List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(1); }); @@ -160,11 +160,11 @@ private String getSseEndpoint(HttpClientSseClientTransport transport) { } @Configuration - static class CustomObjectMapperConfiguration { + static class CustomJsonMapperConfiguration { @Bean - ObjectMapper objectMapper() { - return new ObjectMapper(); + JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationTests.java index d0a004f2ac..ede7cd76d0 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationTests.java @@ -19,9 +19,9 @@ import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration; @@ -109,11 +109,11 @@ void customEndpointIsRespected() { } @Test - void customObjectMapperIsUsed() { - this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) + void customJsonMapperIsUsed() { + this.applicationContext.withUserConfiguration(CustomJsonMapperConfiguration.class) .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") .run(context -> { - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + assertThat(context.getBean(JsonMapper.class)).isNotNull(); List transports = context.getBean("streamableHttpHttpClientTransports", List.class); assertThat(transports).hasSize(1); @@ -169,11 +169,11 @@ private String getStreamableHttpEndpoint(HttpClientStreamableHttpTransport trans } @Configuration - static class CustomObjectMapperConfiguration { + static class CustomJsonMapperConfiguration { @Bean - ObjectMapper objectMapper() { - return new ObjectMapper(); + JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java index e31cb0431f..1543e76cc5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.Objects; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpSseClientConnectionDetails; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; @@ -53,7 +53,7 @@ *
    *
  • Creates WebFlux-based SSE transports for configured MCP server connections *
  • Configures WebClient.Builder for HTTP client operations - *
  • Sets up ObjectMapper for JSON serialization/deserialization + *
  • Sets up JsonMapper for JSON serialization/deserialization *
  • Supports multiple named server connections with different base URLs *
* @@ -79,24 +79,23 @@ PropertiesMcpSseClientConnectionDetails mcpSseClientConnectionDetails(McpSseClie * Each transport is configured with: *
    *
  • A cloned WebClient.Builder with server-specific base URL - *
  • ObjectMapper for JSON processing + *
  • JsonMapper for JSON processing *
  • Server connection parameters from properties *
* @param connectionDetails the SSE client properties containing server configurations * @param webClientBuilderProvider the provider for WebClient.Builder - * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * @param jsonMapperProvider the provider for JsonMapper or a new instance if not * available * @return list of named MCP transports */ @Bean public List sseWebFluxClientTransports(McpSseClientConnectionDetails connectionDetails, - ObjectProvider webClientBuilderProvider, - ObjectProvider objectMapperProvider) { + ObjectProvider webClientBuilderProvider, ObjectProvider jsonMapperProvider) { List sseTransports = new ArrayList<>(); var webClientBuilderTemplate = webClientBuilderProvider.getIfAvailable(WebClient::builder); - var objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + var jsonMapper = jsonMapperProvider.getIfAvailable(JsonMapper::shared); for (Map.Entry serverParameters : connectionDetails.getConnections().entrySet()) { String url = Objects.requireNonNull(serverParameters.getValue().url(), @@ -105,7 +104,7 @@ public List sseWebFluxClientTransports(McpSseClientConn String sseEndpoint = Objects.requireNonNullElse(serverParameters.getValue().sseEndpoint(), "/sse"); var transport = WebFluxSseClientTransport.builder(webClientBuilder) .sseEndpoint(sseEndpoint) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .build(); sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java index cc6318d46e..1871d8dc78 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.Objects; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; @@ -52,7 +52,7 @@ *
  • Creates WebFlux-based Streamable HTTP transports for configured MCP server * connections *
  • Configures WebClient.Builder for HTTP client operations - *
  • Sets up ObjectMapper for JSON serialization/deserialization + *
  • Sets up JsonMapper for JSON serialization/deserialization *
  • Supports multiple named server connections with different base URLs * * @@ -73,26 +73,25 @@ public class StreamableHttpWebFluxTransportAutoConfiguration { * Each transport is configured with: *
      *
    • A cloned WebClient.Builder with server-specific base URL - *
    • ObjectMapper for JSON processing + *
    • JsonMapper for JSON processing *
    • Server connection parameters from properties *
    * @param streamableProperties the Streamable HTTP client properties containing server * configurations * @param webClientBuilderProvider the provider for WebClient.Builder - * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * @param jsonMapperProvider the provider for JsonMapper or a new instance if not * available * @return list of named MCP transports */ @Bean public List streamableHttpWebFluxClientTransports( McpStreamableHttpClientProperties streamableProperties, - ObjectProvider webClientBuilderProvider, - ObjectProvider objectMapperProvider) { + ObjectProvider webClientBuilderProvider, ObjectProvider jsonMapperProvider) { List streamableHttpTransports = new ArrayList<>(); var webClientBuilderTemplate = webClientBuilderProvider.getIfAvailable(WebClient::builder); - var objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + var jsonMapper = jsonMapperProvider.getIfAvailable(JsonMapper::new); for (Map.Entry serverParameters : streamableProperties.getConnections() .entrySet()) { @@ -103,7 +102,7 @@ public List streamableHttpWebFluxClientTransports( var transport = WebClientStreamableHttpTransport.builder(webClientBuilder) .endpoint(streamableHttpEndpoint) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .build(); streamableHttpTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java index f8809ab08a..f10cbb682a 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java @@ -19,9 +19,9 @@ import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -129,11 +129,11 @@ void customWebClientBuilderIsUsed() { } @Test - void customObjectMapperIsUsed() { - this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) + void customJsonMapperIsUsed() { + this.applicationContext.withUserConfiguration(JsonMapperConfiguration.class) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + assertThat(context.getBean(JsonMapper.class)).isNotNull(); List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); }); @@ -194,11 +194,11 @@ WebClient.Builder webClientBuilder() { } @Configuration - static class CustomObjectMapperConfiguration { + static class JsonMapperConfiguration { @Bean - ObjectMapper objectMapper() { - return new ObjectMapper(); + JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java index 9551b41b87..5099a7fce2 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java @@ -19,9 +19,9 @@ import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -136,11 +136,11 @@ void customWebClientBuilderIsUsed() { } @Test - void customObjectMapperIsUsed() { - this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) + void customJsonMapperIsUsed() { + this.applicationContext.withUserConfiguration(CustomJsonMapperConfiguration.class) .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") .run(context -> { - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + assertThat(context.getBean(JsonMapper.class)).isNotNull(); List transports = context.getBean("streamableHttpWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); @@ -206,11 +206,11 @@ WebClient.Builder webClientBuilder() { } @Configuration - static class CustomObjectMapperConfiguration { + static class CustomJsonMapperConfiguration { @Bean - ObjectMapper objectMapper() { - return new ObjectMapper(); + JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java index 1caa746e7b..f4bc515b66 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java @@ -22,8 +22,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpServer; @@ -48,6 +47,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProviderBase; import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import reactor.core.publisher.Mono; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.customizer.McpAsyncServerCustomizer; import org.springframework.ai.mcp.customizer.McpSyncServerCustomizer; @@ -97,8 +97,8 @@ public class McpServerAutoConfiguration { @Bean @ConditionalOnMissingBean public McpServerTransportProviderBase stdioServerTransport( - @Qualifier("mcpServerObjectMapper") ObjectMapper mcpServerObjectMapper) { - return new StdioServerTransportProvider(new JacksonMcpJsonMapper(mcpServerObjectMapper)); + @Qualifier("mcpServerJsonMapper") JsonMapper mcpServerJsonMapper) { + return new StdioServerTransportProvider(new JacksonMcpJsonMapper(mcpServerJsonMapper)); } @Bean diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfiguration.java similarity index 64% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfiguration.java index 2bd59ecc66..64781eefdb 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfiguration.java @@ -16,12 +16,10 @@ package org.springframework.ai.mcp.server.common.autoconfigure; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.ai.util.JacksonUtils; @@ -35,37 +33,34 @@ @ConditionalOnClass(McpSchema.class) @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) -@ConditionalOnMissingBean(name = "mcpServerObjectMapper") -public class McpServerObjectMapperAutoConfiguration { +@ConditionalOnMissingBean(name = "mcpServerJsonMapper") +public class McpServerJsonMapperAutoConfiguration { /** - * Creates a configured ObjectMapper for MCP server JSON serialization. + * Creates a configured {@link JsonMapper} for MCP server JSON serialization. *

    - * This ObjectMapper is specifically configured for MCP protocol compliance with: + * This JsonMapper is specifically configured for MCP protocol compliance with: *

      *
    • Lenient deserialization that doesn't fail on unknown properties
    • *
    • Proper handling of empty beans during serialization
    • *
    • Exclusion of null values from JSON output
    • - *
    • Standard Jackson modules for Java 8, JSR-310, and Kotlin support
    • + *
    • Jackson modules via service loader
    • *
    *

    - * This bean can be overridden by providing a custom ObjectMapper bean with the name - * "mcpServerObjectMapper". - * @return configured ObjectMapper instance for MCP server operations + * This bean can be overridden by providing a custom {@link JsonMapper} bean with the + * name "mcpServerJsonMapper". + * @return configured {@link JsonMapper} instance for MCP server operations */ // NOTE: defaultCandidate=false prevents this MCP specific mapper from being injected // in code that doesn't explicitly qualify injection point by name. - @Bean(name = "mcpServerObjectMapper", defaultCandidate = false) - public ObjectMapper mcpServerObjectMapper() { + @Bean(name = "mcpServerJsonMapper", defaultCandidate = false) + public JsonMapper mcpServerJsonMapper() { return JsonMapper.builder() // Deserialization configuration - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) // Serialization configuration .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .serializationInclusion(JsonInclude.Include.NON_NULL) - // Register standard Jackson modules (Jdk8, JavaTime, ParameterNames, Kotlin) + // Register Jackson modules via server loader .addModules(JacksonUtils.instantiateAvailableModules()) .build(); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 743fa101a6..40385ac596 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -14,7 +14,7 @@ # limitations under the License. # org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration -org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration +org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration org.springframework.ai.mcp.server.common.autoconfigure.StatelessToolCallbackConverterAutoConfiguration diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java index 35fe7a6b55..e5eae8e6ee 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java @@ -74,7 +74,7 @@ public class McpServerAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class)); @Test void defaultConfiguration() { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfigurationIT.java similarity index 60% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfigurationIT.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfigurationIT.java index bbe50dd878..a33c16180f 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerJsonMapperAutoConfigurationIT.java @@ -16,8 +16,8 @@ package org.springframework.ai.mcp.server.common.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.annotation.UserConfigurations; @@ -28,41 +28,41 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link McpServerObjectMapperAutoConfiguration} + * Integration tests for {@link McpServerJsonMapperAutoConfiguration} * * @author guan xu */ -public class McpServerObjectMapperAutoConfigurationIT { +public class McpServerJsonMapperAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(McpServerObjectMapperAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(McpServerJsonMapperAutoConfiguration.class)); @Test - void defaultMcpServerObjectMapper() { + void defaultMcpServerJsonMapper() { this.contextRunner.run(context -> { - assertThat(context).hasSingleBean(ObjectMapper.class); - assertThat(context).hasBean("mcpServerObjectMapper"); + assertThat(context).hasSingleBean(JsonMapper.class); + assertThat(context).hasBean("mcpServerJsonMapper"); }); } @Test - void customizeMcpServerObjectMapper() { + void customizeMcpServerJsonMapper() { this.contextRunner.withConfiguration(UserConfigurations.of(TestConfig.class)).run(context -> { - assertThat(context).hasSingleBean(ObjectMapper.class); - assertThat(context).hasBean("mcpServerObjectMapper"); + assertThat(context).hasSingleBean(JsonMapper.class); + assertThat(context).hasBean("mcpServerJsonMapper"); - var mcpServerObjectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); - var customizedMcpServerObjectMapper = context.getBean(TestConfig.class).mcpServerObjectMapper(); - assertThat(customizedMcpServerObjectMapper).isSameAs(mcpServerObjectMapper); + var mcpServerJsonMapper = context.getBean("mcpServerJsonMapper", JsonMapper.class); + var customizedMcpServerJsonMapper = context.getBean(TestConfig.class).mcpServerJsonMapper(); + assertThat(customizedMcpServerJsonMapper).isSameAs(mcpServerJsonMapper); }); } @Configuration static class TestConfig { - @Bean(name = "mcpServerObjectMapper") - ObjectMapper mcpServerObjectMapper() { - return new ObjectMapper(); + @Bean(name = "mcpServerJsonMapper") + JsonMapper mcpServerJsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java index b84cbf7ef5..b893decc69 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpSyncServer; @@ -29,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -46,35 +46,35 @@ public class McpToolWithStdioIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); /** - * Verifies that a configured ObjectMapper bean is created for MCP server operations. + * Verifies that a configured JsonMapper bean is created for MCP server operations. */ @Test - void shouldCreateConfiguredObjectMapperForMcpServer() { + void shouldCreateConfiguredJsonMapperForMcpServer() { this.contextRunner.run(context -> { - assertThat(context).hasSingleBean(ObjectMapper.class); - ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); + assertThat(context).hasSingleBean(JsonMapper.class); + JsonMapper jsonMapper = context.getBean("mcpServerJsonMapper", JsonMapper.class); - assertThat(objectMapper).isNotNull(); + assertThat(jsonMapper).isNotNull(); - // Verify that the ObjectMapper is properly configured - String emptyBeanJson = objectMapper.writeValueAsString(new EmptyBean()); + // Verify that the JsonMapper is properly configured + String emptyBeanJson = jsonMapper.writeValueAsString(new EmptyBean()); assertThat(emptyBeanJson).isEqualTo("{}"); // Should not fail on empty beans - String nullValueJson = objectMapper.writeValueAsString(new BeanWithNull()); + String nullValueJson = jsonMapper.writeValueAsString(new BeanWithNull()); assertThat(nullValueJson).doesNotContain("null"); // Should exclude null // values }); } /** - * Verifies that STDIO transport uses the configured ObjectMapper. + * Verifies that STDIO transport uses the configured JsonMapper. */ @Test - void stdioTransportShouldUseConfiguredObjectMapper() { + void stdioTransportShouldUseConfiguredJsonMapper() { this.contextRunner.run(context -> { assertThat(context).hasSingleBean(McpServerTransportProviderBase.class); assertThat(context.getBean(McpServerTransportProviderBase.class)) @@ -114,7 +114,7 @@ void mcpToolAnnotationsShouldWorkWithStdio() { assertThat(toolNames).containsExactlyInAnyOrder("add", "subtract", "multiply"); // Verify that each tool has a valid inputSchema that can be serialized - ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); + JsonMapper jsonMapper = context.getBean("mcpServerJsonMapper", JsonMapper.class); for (AsyncToolSpecification spec : tools) { McpSchema.Tool tool = spec.tool(); @@ -125,11 +125,11 @@ void mcpToolAnnotationsShouldWorkWithStdio() { // Verify inputSchema can be serialized to JSON without errors if (tool.inputSchema() != null) { - String schemaJson = objectMapper.writeValueAsString(tool.inputSchema()); + String schemaJson = jsonMapper.writeValueAsString(tool.inputSchema()); assertThat(schemaJson).isNotBlank(); // Should be valid JSON - objectMapper.readTree(schemaJson); + jsonMapper.readTree(schemaJson); } } }); @@ -156,8 +156,8 @@ void mcpToolWithComplexParametersShouldWorkWithStdio() { assertThat(spec.tool().name()).isEqualTo("processData"); // Verify the tool can be serialized - ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); - String toolJson = objectMapper.writeValueAsString(spec.tool()); + JsonMapper jsonMapper = context.getBean("mcpServerJsonMapper", JsonMapper.class); + String toolJson = jsonMapper.writeValueAsString(spec.tool()); assertThat(toolJson).isNotBlank(); }); } @@ -198,7 +198,7 @@ public String processData(@McpToolParam(description = "Input data", required = t } - // Test beans for ObjectMapper configuration verification + // Test beans for JsonMapper configuration verification static class EmptyBean { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfiguration.java index 23d7483d5d..77698617a9 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -82,11 +82,11 @@ public class McpServerSseWebFluxAutoConfiguration { @Bean @ConditionalOnMissingBean - public WebFluxSseServerTransportProvider webFluxTransport( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, McpServerSseProperties serverProperties) { + public WebFluxSseServerTransportProvider webFluxTransport(@Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, + McpServerSseProperties serverProperties) { return WebFluxSseServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .basePath(serverProperties.getBaseUrl()) .messageEndpoint(serverProperties.getSseMessageEndpoint()) .sseEndpoint(serverProperties.getSseEndpoint()) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfiguration.java index 08132f4573..479d9c037a 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -47,11 +47,11 @@ public class McpServerStatelessWebFluxAutoConfiguration { @Bean @ConditionalOnMissingBean public WebFluxStatelessServerTransport webFluxStatelessServerTransport( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, + @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, McpServerStreamableHttpProperties serverProperties) { return WebFluxStatelessServerTransport.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .messageEndpoint(serverProperties.getMcpEndpoint()) .build(); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfiguration.java index 99a9df0784..c5b4cbd1b1 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/main/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -48,11 +48,11 @@ public class McpServerStreamableHttpWebFluxAutoConfiguration { @Bean @ConditionalOnMissingBean public WebFluxStreamableServerTransportProvider webFluxStreamableServerTransportProvider( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, + @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, McpServerStreamableHttpProperties serverProperties) { return WebFluxStreamableServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .messageEndpoint(serverProperties.getMcpEndpoint()) .keepAliveInterval(serverProperties.getKeepAliveInterval()) .disallowDelete(serverProperties.isDisallowDelete()) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationIT.java index 87a45daeb6..ee19ea9f43 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationIT.java @@ -16,13 +16,13 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -36,7 +36,7 @@ class McpServerSseWebFluxAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpServerSseWebFluxAutoConfiguration.class, - McpServerAutoConfiguration.class, McpServerObjectMapperAutoConfiguration.class)); + McpServerAutoConfiguration.class, McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -72,8 +72,8 @@ void endpointConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebFluxSseServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); }); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationTests.java index e38c8cd338..aacd488402 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerSseWebFluxAutoConfigurationTests.java @@ -16,11 +16,11 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.json.JsonMapper; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -34,21 +34,21 @@ class McpServerSseWebFluxAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpServerSseWebFluxAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, TestConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class, TestConfiguration.class)); @Test - void shouldConfigureWebFluxTransportWithCustomObjectMapper() { + void shouldConfigureWebFluxTransportWithCustomJsonMapper() { this.contextRunner.run(context -> { assertThat(context).hasSingleBean(WebFluxSseServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); assertThat(context).hasSingleBean(McpServerProperties.class); - JsonMapper jsonMapper = context.getBean("mcpServerObjectMapper", JsonMapper.class); + JsonMapper jsonMapper = context.getBean("mcpServerJsonMapper", JsonMapper.class); // Verify that the JsonMapper is configured to ignore unknown properties - assertThat(jsonMapper - .isEnabled(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + assertThat(jsonMapper.isEnabled(tools.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) + .isFalse(); // Test with a JSON payload containing unknown fields // CHECKSTYLE:OFF diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfigurationIT.java index a366113a4a..fdf8427481 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStatelessWebFluxAutoConfigurationIT.java @@ -16,12 +16,12 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -37,7 +37,7 @@ class McpServerStatelessWebFluxAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STATELESS") .withConfiguration(AutoConfigurations.of(McpServerStatelessWebFluxAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -48,8 +48,8 @@ void defaultConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebFluxStatelessServerTransport.class); assertThat(context).hasSingleBean(RouterFunction.class); }); @@ -98,13 +98,13 @@ void disallowDeleteFalseConfiguration() { } @Test - void customObjectMapperIsUsed() { - ObjectMapper customObjectMapper = new ObjectMapper(); - this.contextRunner.withBean("customObjectMapper", ObjectMapper.class, () -> customObjectMapper).run(context -> { + void customJsonMapperIsUsed() { + JsonMapper customJsonMapper = new JsonMapper(); + this.contextRunner.withBean("customJsonMapper", JsonMapper.class, () -> customJsonMapper).run(context -> { assertThat(context).hasSingleBean(WebFluxStatelessServerTransport.class); assertThat(context).hasSingleBean(RouterFunction.class); - // Verify the custom ObjectMapper is used - assertThat(context.getBean(ObjectMapper.class)).isSameAs(customObjectMapper); + // Verify the custom JsonMapper is used + assertThat(context.getBean(JsonMapper.class)).isSameAs(customJsonMapper); }); } @@ -123,7 +123,7 @@ void conditionalOnMissingBeanWorks() { this.contextRunner .withBean("customWebFluxProvider", WebFluxStatelessServerTransport.class, () -> WebFluxStatelessServerTransport.builder() - .jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper())) + .jsonMapper(new JacksonMcpJsonMapper(new JsonMapper())) .messageEndpoint("/custom") .build()) .run(context -> { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfigurationIT.java index c222129685..852b301eb5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpServerStreamableHttpWebFluxAutoConfigurationIT.java @@ -16,12 +16,12 @@ package org.springframework.ai.mcp.server.webflux.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.web.reactive.function.server.RouterFunction; @@ -35,7 +35,7 @@ class McpServerStreamableHttpWebFluxAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerStreamableHttpWebFluxAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -46,8 +46,8 @@ void defaultConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebFluxStreamableServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); }); @@ -97,13 +97,13 @@ void disallowDeleteFalseConfiguration() { } @Test - void customObjectMapperIsUsed() { - ObjectMapper customObjectMapper = new ObjectMapper(); - this.contextRunner.withBean("customObjectMapper", ObjectMapper.class, () -> customObjectMapper).run(context -> { + void customJsonMapperIsUsed() { + JsonMapper customJsonMapper = new JsonMapper(); + this.contextRunner.withBean("customJsonMapper", JsonMapper.class, () -> customJsonMapper).run(context -> { assertThat(context).hasSingleBean(WebFluxStreamableServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); - // Verify the custom ObjectMapper is used - assertThat(context.getBean(ObjectMapper.class)).isSameAs(customObjectMapper); + // Verify the custom JsonMapper is used + assertThat(context.getBean(JsonMapper.class)).isSameAs(customJsonMapper); }); } @@ -122,7 +122,7 @@ void conditionalOnMissingBeanWorks() { this.contextRunner .withBean("customWebFluxProvider", WebFluxStreamableServerTransportProvider.class, () -> WebFluxStreamableServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper())) + .jsonMapper(new JacksonMcpJsonMapper(new JsonMapper())) .messageEndpoint("/custom") .build()) .run(context -> { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallProviderCachingIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallProviderCachingIT.java index 207dfea77c..fd29ca30b5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallProviderCachingIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallProviderCachingIT.java @@ -37,7 +37,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -69,7 +69,7 @@ public class McpToolCallProviderCachingIT { private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallbackParameterlessToolIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallbackParameterlessToolIT.java index b0faed0005..68ba634f28 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallbackParameterlessToolIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/McpToolCallbackParameterlessToolIT.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.stream.Stream; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; @@ -31,6 +30,7 @@ import org.junit.jupiter.api.Test; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; @@ -38,7 +38,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -70,7 +70,7 @@ class McpToolCallbackParameterlessToolIT { private final ApplicationContextRunner syncServerContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE", "spring.ai.mcp.server.type=SYNC") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); @@ -100,10 +100,10 @@ void testMcpServerClientIntegrationWithIncompleteSchemaSyncTool() { McpSyncServer mcpSyncServer = serverContext.getBean(McpSyncServer.class); - ObjectMapper objectMapper = serverContext.getBean(ObjectMapper.class); + JsonMapper jsonMapper = serverContext.getBean(JsonMapper.class); String incompleteSchemaJson = "{\"type\":\"object\",\"additionalProperties\":false}"; - McpSchema.JsonSchema incompleteSchema = objectMapper.readValue(incompleteSchemaJson, + McpSchema.JsonSchema incompleteSchema = jsonMapper.readValue(incompleteSchemaJson, McpSchema.JsonSchema.class); // Build the tool using the builder pattern diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java index 6b288c4c63..c4ea8c72fc 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java @@ -25,9 +25,8 @@ import java.util.function.Function; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; @@ -59,6 +58,7 @@ import org.slf4j.LoggerFactory; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration; @@ -66,7 +66,7 @@ import org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.beans.factory.ObjectProvider; @@ -88,10 +88,10 @@ public class SseWebClientWebFluxServerIT { private static final Logger logger = LoggerFactory.getLogger(SseWebClientWebFluxServerIT.class); - private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper()); + private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new JsonMapper()); private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(McpServerAutoConfiguration.class, McpServerObjectMapperAutoConfiguration.class, + AutoConfigurations.of(McpServerAutoConfiguration.class, McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerSseWebFluxAutoConfiguration.class)); private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner().withConfiguration( @@ -446,7 +446,7 @@ public List myResources() { var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents( request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java index 492df0030b..d229061ad3 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java @@ -20,9 +20,8 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpStatelessServerFeatures; import io.modelcontextprotocol.server.McpStatelessSyncServer; import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport; @@ -44,6 +43,7 @@ import org.junit.jupiter.api.Test; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.mcp.McpToolUtils; @@ -52,7 +52,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.StatelessToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; @@ -75,12 +75,12 @@ public class StatelessWebClientWebFluxServerIT { - private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper()); + private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new JsonMapper()); private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STATELESS") .withConfiguration(AutoConfigurations.of(McpServerStatelessAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, StatelessToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, StatelessToolCallbackConverterAutoConfiguration.class, McpServerStatelessWebFluxAutoConfiguration.class)); private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner() @@ -363,7 +363,7 @@ public List myResources() var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents( request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java index 088b1f6d90..5ab0011340 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java @@ -25,7 +25,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; @@ -68,13 +67,14 @@ import org.springaicommunity.mcp.context.StructuredElicitResult; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -100,7 +100,7 @@ public class StreamableMcpAnnotations2IT { private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); @@ -378,7 +378,7 @@ public ReadResourceResult testResource(McpSyncRequestContext ctx, ReadResourceRe try { var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = JsonMapper.shared().writeValueAsString(systemInfo); return new ReadResourceResult(List .of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java index 6261b6b436..cd9d1168e5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java @@ -25,7 +25,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServerExchange; @@ -69,13 +68,14 @@ import org.springaicommunity.mcp.annotation.McpToolParam; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -101,7 +101,7 @@ public class StreamableMcpAnnotationsIT { private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); @@ -383,7 +383,7 @@ public McpSchema.ReadResourceResult testResource(McpSchema.ReadResourceRequest r try { var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = JsonMapper.shared().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult(List .of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java index 50a9ac1921..24e6da50a5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java @@ -25,7 +25,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServerExchange; @@ -70,6 +69,7 @@ import org.springaicommunity.mcp.annotation.McpToolParam; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; @@ -77,7 +77,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -108,7 +108,7 @@ public class StreamableMcpAnnotationsManualIT { .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class, McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class)); private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner() @@ -390,7 +390,7 @@ public McpSchema.ReadResourceResult testResource(McpSchema.ReadResourceRequest r try { var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = JsonMapper.shared().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult(List .of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java index 088877fe43..bb7e10bc74 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java @@ -50,7 +50,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; @@ -87,7 +87,7 @@ public class StreamableMcpAnnotationsWithLLMIT { private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, McpServerSpecificationFactoryAutoConfiguration.class)); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java index cd845a8b13..f979c1c7af 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java @@ -26,9 +26,8 @@ import java.util.function.Function; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; @@ -60,6 +59,7 @@ import org.slf4j.LoggerFactory; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration; @@ -67,7 +67,7 @@ import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties; @@ -90,12 +90,12 @@ public class StreamableWebClientWebFluxServerIT { private static final Logger logger = LoggerFactory.getLogger(StreamableWebClientWebFluxServerIT.class); - private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper()); + private static final JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new JsonMapper()); private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, + McpServerJsonMapperAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class)); private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner() @@ -445,7 +445,7 @@ public List myResources() { var systemInfo = Map.of("os", System.getProperty("os.name"), "os_version", System.getProperty("os.version"), "java_version", System.getProperty("java.version")); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents( request.uri(), "application/json", jsonContent))); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfiguration.java index 5f255bca7b..a8774d42c8 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -76,10 +76,10 @@ public class McpServerSseWebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, McpServerSseProperties serverProperties) { + @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, McpServerSseProperties serverProperties) { return WebMvcSseServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .baseUrl(serverProperties.getBaseUrl()) .sseEndpoint(serverProperties.getSseEndpoint()) .messageEndpoint(serverProperties.getSseMessageEndpoint()) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java index 66691bdd73..cb362f13c6 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebMvcStatelessServerTransport; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -48,11 +48,11 @@ public class McpServerStatelessWebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean public WebMvcStatelessServerTransport webMvcStatelessServerTransport( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, + @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, McpServerStreamableHttpProperties serverProperties) { return WebMvcStatelessServerTransport.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .messageEndpoint(serverProperties.getMcpEndpoint()) .build(); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java index 12dbc7418a..b9b0a503b8 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; @@ -49,11 +49,11 @@ public class McpServerStreamableHttpWebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider( - @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, + @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper, McpServerStreamableHttpProperties serverProperties) { return WebMvcStreamableServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .mcpEndpoint(serverProperties.getMcpEndpoint()) .keepAliveInterval(serverProperties.getKeepAliveInterval()) .disallowDelete(serverProperties.isDisallowDelete()) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfigurationIT.java index 5cc0e953eb..2c1be2e7e8 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerSseWebMvcAutoConfigurationIT.java @@ -16,13 +16,13 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -40,7 +40,7 @@ class McpServerSseWebMvcAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpServerSseWebMvcAutoConfiguration.class, - McpServerAutoConfiguration.class, McpServerObjectMapperAutoConfiguration.class)); + McpServerAutoConfiguration.class, McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -76,8 +76,8 @@ void endpointConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebMvcSseServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); }); @@ -112,7 +112,7 @@ public ConfigurableEnvironment getEnvironment() { return new StandardServletEnvironment(); } }).withConfiguration(AutoConfigurations.of(McpServerSseWebMvcAutoConfiguration.class, - McpServerAutoConfiguration.class, McpServerObjectMapperAutoConfiguration.class)) + McpServerAutoConfiguration.class, McpServerJsonMapperAutoConfiguration.class)) .run(context -> { var mcpSyncServer = context.getBean(McpSyncServer.class); var field = ReflectionUtils.findField(McpSyncServer.class, "immediateExecution"); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfigurationIT.java index a0b0b808a6..5e646cfe30 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStatelessWebMvcAutoConfigurationIT.java @@ -16,12 +16,12 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebMvcStatelessServerTransport; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.web.servlet.function.RouterFunction; @@ -35,7 +35,7 @@ class McpServerStatelessWebMvcAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STATELESS") .withConfiguration(AutoConfigurations.of(McpServerStatelessWebMvcAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -46,8 +46,8 @@ void defaultConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebMvcStatelessServerTransport.class); assertThat(context).hasSingleBean(RouterFunction.class); }); @@ -96,13 +96,13 @@ void disallowDeleteFalseConfiguration() { } @Test - void customObjectMapperIsUsed() { - ObjectMapper customObjectMapper = new ObjectMapper(); - this.contextRunner.withBean("customObjectMapper", ObjectMapper.class, () -> customObjectMapper).run(context -> { + void customjsonMapperIsUsed() { + JsonMapper customJsonMapper = new JsonMapper(); + this.contextRunner.withBean("customJsonMapper", JsonMapper.class, () -> customJsonMapper).run(context -> { assertThat(context).hasSingleBean(WebMvcStatelessServerTransport.class); assertThat(context).hasSingleBean(RouterFunction.class); - // Verify the custom ObjectMapper is used - assertThat(context.getBean(ObjectMapper.class)).isSameAs(customObjectMapper); + // Verify the custom JsonMapper is used + assertThat(context.getBean(JsonMapper.class)).isSameAs(customJsonMapper); }); } @@ -121,7 +121,7 @@ void conditionalOnMissingBeanWorks() { this.contextRunner .withBean("customWebMvcProvider", WebMvcStatelessServerTransport.class, () -> WebMvcStatelessServerTransport.builder() - .jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper())) + .jsonMapper(new JacksonMcpJsonMapper(new JsonMapper())) .messageEndpoint("/custom") .build()) .run(context -> { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfigurationIT.java index 4613c2424a..df77026731 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/test/java/org/springframework/ai/mcp/server/webmvc/autoconfigure/McpServerStreamableHttpWebMvcAutoConfigurationIT.java @@ -16,12 +16,12 @@ package org.springframework.ai.mcp.server.webmvc.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; -import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerJsonMapperAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.web.servlet.function.RouterFunction; @@ -35,7 +35,7 @@ class McpServerStreamableHttpWebMvcAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") .withConfiguration(AutoConfigurations.of(McpServerStreamableHttpWebMvcAutoConfiguration.class, - McpServerObjectMapperAutoConfiguration.class)); + McpServerJsonMapperAutoConfiguration.class)); @Test void defaultConfiguration() { @@ -46,8 +46,8 @@ void defaultConfiguration() { } @Test - void objectMapperConfiguration() { - this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run(context -> { + void jsonMapperConfiguration() { + this.contextRunner.withBean(JsonMapper.class, JsonMapper::new).run(context -> { assertThat(context).hasSingleBean(WebMvcStreamableServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); }); @@ -97,13 +97,13 @@ void disallowDeleteFalseConfiguration() { } @Test - void customObjectMapperIsUsed() { - ObjectMapper customObjectMapper = new ObjectMapper(); - this.contextRunner.withBean("customObjectMapper", ObjectMapper.class, () -> customObjectMapper).run(context -> { + void customJsonMapperIsUsed() { + JsonMapper customJsonMapper = new JsonMapper(); + this.contextRunner.withBean("customJsonMapper", JsonMapper.class, () -> customJsonMapper).run(context -> { assertThat(context).hasSingleBean(WebMvcStreamableServerTransportProvider.class); assertThat(context).hasSingleBean(RouterFunction.class); - // Verify the custom ObjectMapper is used - assertThat(context.getBean(ObjectMapper.class)).isSameAs(customObjectMapper); + // Verify the custom JsonMapper is used + assertThat(context.getBean(JsonMapper.class)).isSameAs(customJsonMapper); }); } @@ -122,7 +122,7 @@ void conditionalOnMissingBeanWorks() { this.contextRunner .withBean("customWebFluxProvider", WebMvcStreamableServerTransportProvider.class, () -> WebMvcStreamableServerTransportProvider.builder() - .jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper())) + .jsonMapper(new JacksonMcpJsonMapper(new JsonMapper())) .mcpEndpoint("/custom") .build()) .run(context -> { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java index 1001b7d5b7..58ffc2bab0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java @@ -16,9 +16,9 @@ package org.springframework.ai.model.bedrock.cohere.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.providers.AwsRegionProvider; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi; @@ -55,9 +55,9 @@ public class BedrockCohereEmbeddingAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { + BedrockAwsConnectionProperties awsProperties, JsonMapper jsonMapper) { return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - objectMapper, awsProperties.getTimeout()); + jsonMapper, awsProperties.getTimeout()); } @Bean diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java index 6d0ed14bfb..f4d0249b95 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.ai.model.bedrock.titan.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.ObservationRegistry; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.providers.AwsRegionProvider; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi; @@ -58,7 +58,7 @@ public class BedrockTitanEmbeddingAutoConfiguration { @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider, BedrockTitanEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { + BedrockAwsConnectionProperties awsProperties, JsonMapper jsonMapper) { // Validate required properties if (properties.getModel() == null || awsProperties.getTimeout() == null) { @@ -66,7 +66,7 @@ public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider } return new TitanEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - objectMapper, awsProperties.getTimeout()); + jsonMapper, awsProperties.getTimeout()); } @Bean diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java index 88b12b6c30..73108a0c91 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java @@ -16,8 +16,8 @@ package org.springframework.ai.model.bedrock.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -45,8 +45,8 @@ public static ApplicationContextRunner getContextRunnerWithUserConfiguration() { static class Config { @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); + public JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java index 3bbd14dbbe..036208c659 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java @@ -16,8 +16,8 @@ package org.springframework.ai.model.bedrock.cohere.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -35,7 +35,7 @@ public class BedrockCohereModelConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) - .withBean(ObjectMapper.class, ObjectMapper::new); + .withBean(JsonMapper.class, JsonMapper::new); @Test void embeddingModelActivation() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java index b66b4c4f91..7d0a06ec17 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java @@ -16,8 +16,8 @@ package org.springframework.ai.model.bedrock.titan.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -35,7 +35,7 @@ public class BedrockTitanModelConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) - .withBean(ObjectMapper.class, ObjectMapper::new); + .withBean(JsonMapper.class, JsonMapper::new); @Test void embeddingModelActivation() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml index 74709b8e8e..37bfe9edac 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml @@ -122,7 +122,7 @@ - com.fasterxml.jackson.module + tools.jackson.module jackson-module-kotlin test diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/main/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfiguration.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/main/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfiguration.java index dad293b85b..f2337d43de 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/main/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfiguration.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/main/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfiguration.java @@ -16,9 +16,9 @@ package org.springframework.ai.vectorstore.chroma.autoconfigure; -import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.ObservationRegistry; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chroma.vectorstore.ChromaApi; import org.springframework.ai.chroma.vectorstore.ChromaVectorStore; @@ -46,7 +46,7 @@ * @author Sebastien Deleuze */ @AutoConfiguration -@ConditionalOnClass({ EmbeddingModel.class, RestClient.class, ChromaVectorStore.class, ObjectMapper.class }) +@ConditionalOnClass({ EmbeddingModel.class, RestClient.class, ChromaVectorStore.class, JsonMapper.class }) @EnableConfigurationProperties({ ChromaApiProperties.class, ChromaVectorStoreProperties.class }) @ConditionalOnProperty(name = SpringAIVectorStoreTypes.TYPE, havingValue = SpringAIVectorStoreTypes.CHROMA, matchIfMissing = true) @@ -62,14 +62,14 @@ PropertiesChromaConnectionDetails chromaConnectionDetails(ChromaApiProperties pr @ConditionalOnMissingBean public ChromaApi chromaApi(ChromaApiProperties apiProperties, ObjectProvider restClientBuilderProvider, ChromaConnectionDetails connectionDetails, - ObjectMapper objectMapper) { + JsonMapper jsonMapper) { String chromaUrl = String.format("%s:%s", connectionDetails.getHost(), connectionDetails.getPort()); var chromaApi = ChromaApi.builder() .baseUrl(chromaUrl) .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .objectMapper(objectMapper) + .jsonMapper(jsonMapper) .build(); if (StringUtils.hasText(connectionDetails.getKeyToken())) { diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/test/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/test/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfigurationIT.java index 9f7e90983a..4158b0716b 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/test/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/test/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfigurationIT.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Disabled; @@ -27,6 +26,7 @@ import org.testcontainers.chromadb.ChromaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; @@ -234,8 +234,8 @@ public EmbeddingModel embeddingModel() { } @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); + public JsonMapper jsonMapper() { + return new JsonMapper(); } } diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationAuthenticationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationAuthenticationIT.java index 9bf3f052e2..89226a29c1 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationAuthenticationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationAuthenticationIT.java @@ -19,8 +19,6 @@ import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; @@ -30,6 +28,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.transformers.TransformersEmbeddingModel; @@ -135,7 +135,7 @@ void ensureGemFireVectorStoreCustomConfiguration() { private Map parseIndex(String json) { try { - JsonNode rootNode = new ObjectMapper().readTree(json); + JsonNode rootNode = JsonMapper.shared().readTree(json); Map indexDetails = new HashMap<>(); if (rootNode.isObject()) { if (rootNode.has("name")) { diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationIT.java index fd6954c2f0..924ad38a9f 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfigurationIT.java @@ -20,8 +20,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; @@ -32,6 +30,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -214,7 +214,7 @@ public void autoConfigurationEnabledWhenTypeIsGemfire() { private Map parseIndex(String json) { try { - JsonNode rootNode = new ObjectMapper().readTree(json); + JsonNode rootNode = JsonMapper.shared().readTree(json); Map indexDetails = new HashMap<>(); if (rootNode.isObject()) { if (rootNode.has("name")) { diff --git a/models/spring-ai-anthropic/pom.xml b/models/spring-ai-anthropic/pom.xml index a40885848f..f8fed0f775 100644 --- a/models/spring-ai-anthropic/pom.xml +++ b/models/spring-ai-anthropic/pom.xml @@ -101,11 +101,11 @@ test - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - test - + + tools.jackson.dataformat + jackson-dataformat-xml + test + diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java index 94b291bd81..911474bcdf 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java @@ -25,7 +25,6 @@ import java.util.Set; import java.util.stream.Collectors; -import com.fasterxml.jackson.core.type.TypeReference; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; @@ -35,6 +34,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import tools.jackson.core.type.TypeReference; import org.springframework.ai.anthropic.api.AnthropicApi; import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage; diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicPromptCachingMockTest.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicPromptCachingMockTest.java index ff4791e57d..7fe182150e 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicPromptCachingMockTest.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicPromptCachingMockTest.java @@ -20,14 +20,14 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.anthropic.api.AnthropicApi; import org.springframework.ai.anthropic.api.AnthropicCacheOptions; @@ -60,8 +60,6 @@ class AnthropicPromptCachingMockTest { private AnthropicChatModel chatModel; - private final ObjectMapper objectMapper = new ObjectMapper(); - @BeforeEach void setUp() throws IOException { this.mockWebServer = new MockWebServer(); @@ -127,7 +125,7 @@ void testSystemOnlyCacheStrategy() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify system message has cache control assertThat(requestBody.has("system")).isTrue(); @@ -182,7 +180,7 @@ void testSystemMinLengthDisablesCaching() throws Exception { RecordedRequest recordedRequest = this.mockWebServer.takeRequest(1, TimeUnit.SECONDS); assertThat(recordedRequest).isNotNull(); - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Ensure no cache_control present since system content was below min length String req = requestBody.toString(); @@ -217,7 +215,7 @@ void testCustomContentLengthFunctionEnablesCaching() throws Exception { RecordedRequest recordedRequest = this.mockWebServer.takeRequest(1, TimeUnit.SECONDS); assertThat(recordedRequest).isNotNull(); - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); JsonNode systemNode = requestBody.get("system"); if (systemNode != null && systemNode.isArray()) { JsonNode lastSystemBlock = systemNode.get(systemNode.size() - 1); @@ -276,7 +274,7 @@ void testSystemAndToolsCacheStrategy() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify tools array exists and last tool has cache control assertThat(requestBody.has("tools")).isTrue(); @@ -343,7 +341,7 @@ void testConversationHistoryCacheStrategy() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify messages array exists assertThat(requestBody.has("messages")).isTrue(); @@ -407,7 +405,7 @@ void testNoCacheStrategy() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify NO cache_control fields exist anywhere String requestBodyString = requestBody.toString(); @@ -534,7 +532,7 @@ void testFourBreakpointLimitEnforcement() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Count cache_control occurrences in the entire request int cacheControlCount = countCacheControlOccurrences(requestBody); @@ -586,7 +584,7 @@ void testWireFormatConsistency() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify that cache_control is included in the wire format for SYSTEM_ONLY // strategy @@ -672,7 +670,7 @@ void testComplexMultiBreakpointScenario() throws Exception { assertThat(recordedRequest).isNotNull(); // Parse and validate request body - JsonNode requestBody = this.objectMapper.readTree(recordedRequest.getBody().readUtf8()); + JsonNode requestBody = JsonMapper.shared().readTree(recordedRequest.getBody().readUtf8()); // Verify system message has cache control (SYSTEM_AND_TOOLS strategy) assertThat(requestBody.has("system")).isTrue(); @@ -710,10 +708,8 @@ private int countCacheControlOccurrences(JsonNode node) { if (node.has("cache_control")) { count++; } - var fields = node.fields(); - while (fields.hasNext()) { - var entry = fields.next(); - count += countCacheControlOccurrences(entry.getValue()); + for (JsonNode child : node.values()) { + count += countCacheControlOccurrences(child); } } else if (node.isArray()) { diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/EventParsingTests.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/EventParsingTests.java index 56af44b78a..8f7d5e634b 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/EventParsingTests.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/EventParsingTests.java @@ -20,11 +20,11 @@ import java.nio.charset.Charset; import java.util.List; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.anthropic.api.AnthropicApi.StreamEvent; import org.springframework.core.io.DefaultResourceLoader; @@ -44,8 +44,7 @@ public void readEvents() throws IOException { String json = new DefaultResourceLoader().getResource("classpath:/sample_events.json") .getContentAsString(Charset.defaultCharset()); - List events = new ObjectMapper().readerFor(new TypeReference<>() { - + List events = JsonMapper.shared().readerFor(new TypeReference<>() { }).readValue(json); logger.info(events.toString()); diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/ChatCompletionRequestSkillsSerializationTests.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/ChatCompletionRequestSkillsSerializationTests.java index b29368f249..31a4c631a3 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/ChatCompletionRequestSkillsSerializationTests.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/ChatCompletionRequestSkillsSerializationTests.java @@ -18,8 +18,8 @@ import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage; import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicSkill; @@ -40,10 +40,8 @@ */ class ChatCompletionRequestSkillsSerializationTests { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Test - void shouldSerializeRequestWithSkills() throws Exception { + void shouldSerializeRequestWithSkills() { SkillContainer container = SkillContainer.builder().skill(AnthropicSkill.XLSX).build(); AnthropicMessage message = new AnthropicMessage(List.of(new ContentBlock("Create a spreadsheet")), Role.USER); @@ -55,7 +53,7 @@ void shouldSerializeRequestWithSkills() throws Exception { .container(container) .build(); - String json = this.objectMapper.writeValueAsString(request); + String json = JsonMapper.shared().writeValueAsString(request); assertThat(json).contains("\"container\""); assertThat(json).contains("\"skills\""); @@ -81,7 +79,7 @@ void shouldSerializeMultipleSkills() throws Exception { .container(container) .build(); - String json = this.objectMapper.writeValueAsString(request); + String json = JsonMapper.shared().writeValueAsString(request); assertThat(json).contains("\"xlsx\""); assertThat(json).contains("\"pptx\""); @@ -99,7 +97,7 @@ void shouldNotIncludeContainerWhenNull() throws Exception { .maxTokens(1024) .build(); - String json = this.objectMapper.writeValueAsString(request); + String json = JsonMapper.shared().writeValueAsString(request); assertThat(json).doesNotContain("\"container\""); } @@ -118,7 +116,7 @@ void shouldSerializeRequestWithSkillsUsingBuilderSkillsMethod() throws Exception .skills(skills) .build(); - String json = this.objectMapper.writeValueAsString(request); + String json = JsonMapper.shared().writeValueAsString(request); assertThat(json).contains("\"container\""); assertThat(json).contains("\"skills\""); @@ -151,7 +149,7 @@ void shouldDeserializeRequestWithSkills() throws Exception { } """; - ChatCompletionRequest request = this.objectMapper.readValue(json, ChatCompletionRequest.class); + ChatCompletionRequest request = JsonMapper.shared().readValue(json, ChatCompletionRequest.class); assertThat(request.container()).isNotNull(); assertThat(request.container().skills()).hasSize(1); diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/AnthropicApiLegacyToolIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/AnthropicApiLegacyToolIT.java deleted file mode 100644 index bfb82ed1a7..0000000000 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/AnthropicApiLegacyToolIT.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.anthropic.api.tool; - -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage; -import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest; -import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse; -import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock; -import org.springframework.ai.anthropic.api.AnthropicApi.Role; -import org.springframework.ai.anthropic.api.tool.XmlHelper.FunctionCalls; -import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools; -import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription; -import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools.ToolDescription.Parameter; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Experiments with - * Anthropic - * Functions & external tools. - * - *

    - * Tool - * Use(Function Calling) with Anthropic's Claude 3 Opus LLM - *

    - * Anthropic - * Functions & external tools - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+") -@SuppressWarnings("null") -public class AnthropicApiLegacyToolIT { - - public static final String TOO_SYSTEM_PROMPT_TEMPLATE = """ - In this environment you have access to a set of tools you can use to answer the user's question. - - You may call them like this: - - - $TOOL_NAME - - <$PARAMETER_NAME>$PARAMETER_VALUE - ... - - - - - Here are the tools available: - %s - """; - - public static final ConcurrentHashMap FUNCTIONS = new ConcurrentHashMap<>(); - - private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class); - - AnthropicApi anthropicApi = AnthropicApi.builder().apiKey(System.getenv("ANTHROPIC_API_KEY")).build(); - - @Test - void toolCalls() { - - String toolDescription = XmlHelper.toXml(new Tools(List.of(new ToolDescription("getCurrentWeather", - "Get the weather in location. Return temperature in 30°F or 30°C format.", - List.of(new Parameter("location", "string", "The city and state e.g. San Francisco, CA"), - new Parameter("unit", "enum", "Temperature unit. Use only C or F. Default is C.")))))); - - logger.info("TOOLS: " + toolDescription); - - String systemPrompt = String.format(TOO_SYSTEM_PROMPT_TEMPLATE, toolDescription); - - AnthropicMessage chatCompletionMessage = new AnthropicMessage( - List.of(new ContentBlock("What's the weather like in Paris? Show the temperature in Celsius.")), - // "What's the weather like in San Francisco, Tokyo, and Paris? Show the - // temperature in Celsius.")), - Role.USER); - - ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest( - AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue(), List.of(chatCompletionMessage), systemPrompt, 500, - 0.8, false); - - ResponseEntity chatCompletion = doCall(chatCompletionRequest); - - var responseText = chatCompletion.getBody().content().get(0).text(); - logger.info("FINAL RESPONSE: " + responseText); - - assertThat(responseText).contains("15"); - } - - private ResponseEntity doCall(ChatCompletionRequest chatCompletionRequest) { - - ResponseEntity response = this.anthropicApi.chatCompletionEntity(chatCompletionRequest); - - FunctionCalls functionCalls = XmlHelper.extractFunctionCalls(response.getBody().content().get(0).text()); - - if (functionCalls == null) { - return response; - } - - logger.info("FunctionCalls from the LLM: " + functionCalls); - - MockWeatherService.Request request = ModelOptionsUtils.mapToClass(functionCalls.invoke().parameters(), - MockWeatherService.Request.class); - - logger.info("Resolved function request param: " + request); - - Object functionCallResponseData = FUNCTIONS.get(functionCalls.invoke().toolName()).apply(request); - - XmlHelper.FunctionResults functionResults = new XmlHelper.FunctionResults(List - .of(new XmlHelper.FunctionResults.Result(functionCalls.invoke().toolName(), functionCallResponseData))); - - String content = XmlHelper.toXml(functionResults); - - logger.info("Function response XML : " + content); - - AnthropicMessage chatCompletionMessage2 = new AnthropicMessage(List.of(new ContentBlock(content)), Role.USER); - - return doCall(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue(), - List.of(chatCompletionMessage2), null, 500, 0.8, false)); - } - - static { - FUNCTIONS.put("getCurrentWeather", new MockWeatherService()); - } - -} diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/XmlHelper.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/XmlHelper.java deleted file mode 100644 index b6c972fc42..0000000000 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/tool/XmlHelper.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2023-2024 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.anthropic.api.tool; - -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; - -import org.springframework.util.StringUtils; - -/** - * @author Christian Tzolov - */ -public final class XmlHelper { - - // Regular expression to match XML block between and - // tags - private static final String FUNCTION_CALLS_REGEX = ".*?"; - - // Compile the regular expression pattern - private static final Pattern FUNCTION_CALLS_PATTERN = Pattern.compile(FUNCTION_CALLS_REGEX, Pattern.DOTALL); - - private static final XmlMapper xmlMapper = new XmlMapper(); - - private XmlHelper() { - - } - - public static String extractFunctionCallsXmlBlock(String text) { - if (!StringUtils.hasText(text)) { - return ""; - } - - Matcher matcher = FUNCTION_CALLS_PATTERN.matcher(text); - - // Find and print the XML block - return (matcher.find()) ? matcher.group() : ""; - } - - public static FunctionCalls extractFunctionCalls(String text) { - - String xml = extractFunctionCallsXmlBlock(text); - - if (!StringUtils.hasText(xml)) { - return null; - } - - try { - FunctionCalls functionCalls = xmlMapper.readValue(xml, FunctionCalls.class); - return functionCalls; - } - catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - public static String toXml(Object object) { - try { - return xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); - } - catch (JsonProcessingException e) { - e.printStackTrace(); - return ""; - } - } - - public static void main(String[] args) throws JsonMappingException, JsonProcessingException { - - String sample = """ - - - getCurrentWeather - - San Francisco, CA - Celsius - - - - """; - - System.out.println(extractFunctionCalls(sample)); - - var toolDescription = new Tools.ToolDescription("getCurrentWeather", - "Get the weather in location. Return temperature in 30°F or 30°C format.", - List.of(new Tools.ToolDescription.Parameter("location", "string", - "The city and state e.g. San Francisco, CA"), - new Tools.ToolDescription.Parameter("unit", "enum", "Temperature unit"))); - - System.out.println(toXml(new Tools(List.of(toolDescription)))); - - } - - @JsonInclude(Include.NON_NULL) // @formatter:off - @JacksonXmlRootElement(localName = "tools") - public record Tools( - @JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("tool_description") List toolDescriptions) { - - public record ToolDescription( - @JsonProperty("tool_name") String toolName, - @JsonProperty("description") String description, - @JacksonXmlElementWrapper(localName = "parameters") @JsonProperty("parameter") List parameters) { - - @JacksonXmlRootElement(localName = "parameter") - public record Parameter( - @JsonProperty("name") String name, - @JsonProperty("type") String type, - @JsonProperty("description") String description) { - } - } - } - // @formatter:on - - @JsonInclude(Include.NON_NULL) // @formatter:off - @JacksonXmlRootElement(localName = "function_calls") - public record FunctionCalls(@JsonProperty("invoke") Invoke invoke) { - public record Invoke( - @JsonProperty("tool_name") String toolName, - @JsonProperty("parameters") Map parameters) { - } - } // @formatter:on - - @JsonInclude(Include.NON_NULL) // @formatter:off - @JacksonXmlRootElement(localName = "function_results") - public record FunctionResults( - @JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("result") List result) { - - public record Result( - @JsonProperty("tool_name") String toolName, - @JsonProperty("stdout") Object stdout) { - } - } // @formatter:on - -} diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java index 88fe6ae6e5..43f16fe0c1 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java @@ -24,13 +24,11 @@ import com.azure.ai.openai.models.ImageGenerationResponseFormat; import com.azure.ai.openai.models.ImageGenerationStyle; import com.azure.ai.openai.models.ImageSize; -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 com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata; import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageResponseMetadata; @@ -64,7 +62,7 @@ public class AzureOpenAiImageModel implements ImageModel { private final AzureOpenAiImageOptions defaultOptions; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; public AzureOpenAiImageModel(OpenAIClient openAIClient) { this(openAIClient, AzureOpenAiImageOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); @@ -75,9 +73,8 @@ public AzureOpenAiImageModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiImag Assert.notNull(options, "AzureOpenAiChatOptions must not be null"); this.openAIClient = microsoftOpenAiClient; this.defaultOptions = options; - this.objectMapper = JsonMapper.builder() + this.jsonMapper = JsonMapper.builder() .addModules(JacksonUtils.instantiateAvailableModules()) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .build(); } @@ -113,9 +110,9 @@ public ImageResponse call(ImagePrompt imagePrompt) { private String toPrettyJson(Object object) { try { - return this.objectMapper.writeValueAsString(object); + return this.jsonMapper.writeValueAsString(object); } - catch (JsonProcessingException e) { + catch (JacksonException e) { return "JsonProcessingException:" + e + " [" + object.toString() + "]"; } } diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/api/AbstractBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/api/AbstractBedrockApi.java index a1236b4c40..2ad83fc90d 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/api/AbstractBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/api/AbstractBedrockApi.java @@ -18,7 +18,6 @@ // @formatter:off -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -26,8 +25,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -46,6 +43,8 @@ import software.amazon.awssdk.services.bedrockruntime.model.InvokeModelWithResponseStreamRequest; import software.amazon.awssdk.services.bedrockruntime.model.InvokeModelWithResponseStreamResponseHandler; import software.amazon.awssdk.services.bedrockruntime.model.ResponseStream; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.util.Assert; @@ -81,7 +80,7 @@ public abstract class AbstractBedrockApi { private final String modelId; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private final Region region; private final BedrockRuntimeClient client; private final BedrockRuntimeAsyncClient clientStreaming; @@ -93,7 +92,7 @@ public abstract class AbstractBedrockApi { * @param region The AWS region to use. */ public AbstractBedrockApi(String modelId, String region) { - this(modelId, ProfileCredentialsProvider.builder().build(), region, ModelOptionsUtils.OBJECT_MAPPER, Duration.ofMinutes(5)); + this(modelId, ProfileCredentialsProvider.builder().build(), region, ModelOptionsUtils.JSON_MAPPER, Duration.ofMinutes(5)); } /** * Create a new AbstractBedrockApi instance using default credentials provider and object mapper. @@ -103,7 +102,7 @@ public AbstractBedrockApi(String modelId, String region) { * @param timeout The timeout to use. */ public AbstractBedrockApi(String modelId, String region, Duration timeout) { - this(modelId, ProfileCredentialsProvider.builder().build(), region, ModelOptionsUtils.OBJECT_MAPPER, timeout); + this(modelId, ProfileCredentialsProvider.builder().build(), region, ModelOptionsUtils.JSON_MAPPER, timeout); } /** @@ -112,11 +111,11 @@ public AbstractBedrockApi(String modelId, String region, Duration timeout) { * @param modelId The model id to use. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param jsonMapper The JSON mapper to use for JSON serialization and deserialization. */ public AbstractBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, - ObjectMapper objectMapper) { - this(modelId, credentialsProvider, region, objectMapper, Duration.ofMinutes(5)); + JsonMapper jsonMapper) { + this(modelId, credentialsProvider, region, jsonMapper, Duration.ofMinutes(5)); } /** @@ -125,37 +124,37 @@ public AbstractBedrockApi(String modelId, AwsCredentialsProvider credentialsProv * @param modelId The model id to use. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param jsonMapper The JSON mapper to use for JSON serialization and deserialization. * @param timeout Configure the amount of time to allow the client to complete the execution of an API call. * This timeout covers the entire client execution except for marshalling. This includes request handler execution, * all HTTP requests including retries, unmarshalling, etc. This value should always be positive, if present. */ public AbstractBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, - ObjectMapper objectMapper, Duration timeout) { - this(modelId, credentialsProvider, Region.of(region), objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + this(modelId, credentialsProvider, Region.of(region), jsonMapper, timeout); } /** - * Create a new AbstractBedrockApi instance using the provided credentials provider, region and object mapper. + * Create a new AbstractBedrockApi instance using the provided credentials provider, region and JSON mapper. * * @param modelId The model id to use. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param jsonMapper The JSON mapper to use for JSON serialization and deserialization. * @param timeout Configure the amount of time to allow the client to complete the execution of an API call. * This timeout covers the entire client execution except for marshalling. This includes request handler execution, * all HTTP requests including retries, unmarshalling, etc. This value should always be positive, if present. */ public AbstractBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, Region region, - ObjectMapper objectMapper, Duration timeout) { + JsonMapper jsonMapper, Duration timeout) { Assert.hasText(modelId, "Model id must not be empty"); Assert.notNull(credentialsProvider, "Credentials provider must not be null"); - Assert.notNull(objectMapper, "Object mapper must not be null"); + Assert.notNull(jsonMapper, "JSON mapper must not be null"); Assert.notNull(timeout, "Timeout must not be null"); this.modelId = modelId; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.region = getRegion(region); this.client = BedrockRuntimeClient.builder() @@ -233,9 +232,9 @@ protected O internalInvocation(I request, Class clazz) { SdkBytes body; try { - body = SdkBytes.fromUtf8String(this.objectMapper.writeValueAsString(request)); + body = SdkBytes.fromUtf8String(this.jsonMapper.writeValueAsString(request)); } - catch (JsonProcessingException e) { + catch (JacksonException e) { throw new IllegalArgumentException("Invalid JSON format for the input request: " + request, e); } @@ -249,10 +248,9 @@ protected O internalInvocation(I request, Class clazz) { String responseBody = response.body().asString(StandardCharsets.UTF_8); try { - return this.objectMapper.readValue(responseBody, clazz); + return this.jsonMapper.readValue(responseBody, clazz); } - catch (JsonProcessingException | UncheckedIOException e) { - + catch (JacksonException e) { throw new IllegalArgumentException("Invalid JSON format for the response: " + responseBody, e); } } @@ -271,9 +269,9 @@ protected Flux internalInvocationStream(I request, Class clazz) { SdkBytes body; try { - body = SdkBytes.fromUtf8String(this.objectMapper.writeValueAsString(request)); + body = SdkBytes.fromUtf8String(this.jsonMapper.writeValueAsString(request)); } - catch (JsonProcessingException e) { + catch (JacksonException e) { eventSink.emitError(e, DEFAULT_EMIT_FAILURE_HANDLER); return eventSink.asFlux(); } @@ -288,10 +286,10 @@ protected Flux internalInvocationStream(I request, Class clazz) { .onChunk(chunk -> { try { logger.debug("Received chunk: {}", chunk.bytes().asString(StandardCharsets.UTF_8)); - SO response = this.objectMapper.readValue(chunk.bytes().asByteArray(), clazz); + SO response = this.jsonMapper.readValue(chunk.bytes().asByteArray(), clazz); eventSink.emitNext(response, DEFAULT_EMIT_FAILURE_HANDLER); } - catch (Exception e) { + catch (JacksonException e) { logger.error("Failed to unmarshall", e); eventSink.emitError(e, DEFAULT_EMIT_FAILURE_HANDLER); } diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApi.java index c8e75c52ad..30ccde5380 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApi.java @@ -23,9 +23,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.api.AbstractBedrockApi; import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest; @@ -62,12 +62,12 @@ public CohereEmbeddingBedrockApi(String modelId, String region) { * supported models. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and + * @param jsonMapper The JSON mapper to use for JSON serialization and * deserialization. */ public CohereEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, - ObjectMapper objectMapper) { - super(modelId, credentialsProvider, region, objectMapper); + JsonMapper jsonMapper) { + super(modelId, credentialsProvider, region, jsonMapper); } /** @@ -89,29 +89,29 @@ public CohereEmbeddingBedrockApi(String modelId, String region, Duration timeout * supported models. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and + * @param jsonMapper The JSON mapper to use for JSON serialization and * deserialization. * @param timeout The timeout to use. */ public CohereEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, - ObjectMapper objectMapper, Duration timeout) { - super(modelId, credentialsProvider, region, objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + super(modelId, credentialsProvider, region, jsonMapper, timeout); } /** * Create a new CohereEmbeddingBedrockApi instance using the provided credentials - * provider, region and object mapper. + * provider, region and JSON mapper. * @param modelId The model id to use. See the {@link CohereEmbeddingModel} for the * supported models. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and + * @param jsonMapper The JSON mapper to use for JSON serialization and * deserialization. * @param timeout The timeout to use. */ public CohereEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, Region region, - ObjectMapper objectMapper, Duration timeout) { - super(modelId, credentialsProvider, region, objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + super(modelId, credentialsProvider, region, jsonMapper, timeout); } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApi.java index b97e860048..8095af2784 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApi.java @@ -23,9 +23,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.api.AbstractBedrockApi; import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingRequest; @@ -61,12 +61,12 @@ public TitanEmbeddingBedrockApi(String modelId, String region, Duration timeout) * @param modelId The model id to use. See the {@link TitanEmbeddingModel} for the supported models. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param jsonMapper The JSON mapper to use for JSON serialization and deserialization. * @param timeout The timeout to use. */ public TitanEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, - ObjectMapper objectMapper, Duration timeout) { - super(modelId, credentialsProvider, region, objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + super(modelId, credentialsProvider, region, jsonMapper, timeout); } /** @@ -75,12 +75,12 @@ public TitanEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentia * @param modelId The model id to use. See the {@link TitanEmbeddingModel} for the supported models. * @param credentialsProvider The credentials provider to connect to AWS. * @param region The AWS region to use. - * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param jsonMapper The JSON mapper to use for JSON serialization and deserialization. * @param timeout The timeout to use. */ public TitanEmbeddingBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, Region region, - ObjectMapper objectMapper, Duration timeout) { - super(modelId, credentialsProvider, region, objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + super(modelId, credentialsProvider, region, jsonMapper, timeout); } @Override diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/api/AbstractBedrockApiTest.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/api/AbstractBedrockApiTest.java index 1a6f20d88b..a133f5223b 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/api/AbstractBedrockApiTest.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/api/AbstractBedrockApiTest.java @@ -18,7 +18,6 @@ import java.time.Duration; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; @@ -29,6 +28,7 @@ import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -46,7 +46,7 @@ class AbstractBedrockApiTest { private AwsCredentialsProvider awsCredentialsProvider = mock(AwsCredentialsProvider.class); @Mock - private ObjectMapper objectMapper = mock(ObjectMapper.class); + private JsonMapper jsonMapper = mock(JsonMapper.class); @Test void shouldLoadRegionFromAwsDefaults() { @@ -54,7 +54,7 @@ void shouldLoadRegionFromAwsDefaults() { when(this.awsRegionProviderBuilder.build().getRegion()).thenReturn(Region.AF_SOUTH_1); mocked.when(DefaultAwsRegionProviderChain::builder).thenReturn(this.awsRegionProviderBuilder); AbstractBedrockApi testBedrockApi = new TestBedrockApi("modelId", - this.awsCredentialsProvider, null, this.objectMapper, Duration.ofMinutes(5)); + this.awsCredentialsProvider, null, this.jsonMapper, Duration.ofMinutes(5)); assertThat(testBedrockApi.getRegion()).isEqualTo(Region.AF_SOUTH_1); } } @@ -65,7 +65,7 @@ void shouldThrowIllegalArgumentIfAwsDefaultsFailed() { when(this.awsRegionProviderBuilder.build().getRegion()) .thenThrow(SdkClientException.builder().message("failed load").build()); mocked.when(DefaultAwsRegionProviderChain::builder).thenReturn(this.awsRegionProviderBuilder); - assertThatThrownBy(() -> new TestBedrockApi("modelId", this.awsCredentialsProvider, null, this.objectMapper, + assertThatThrownBy(() -> new TestBedrockApi("modelId", this.awsCredentialsProvider, null, this.jsonMapper, Duration.ofMinutes(5))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("failed load"); @@ -75,8 +75,8 @@ void shouldThrowIllegalArgumentIfAwsDefaultsFailed() { private static class TestBedrockApi extends AbstractBedrockApi { protected TestBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, Region region, - ObjectMapper objectMapper, Duration timeout) { - super(modelId, credentialsProvider, region, objectMapper, timeout); + JsonMapper jsonMapper, Duration timeout) { + super(modelId, credentialsProvider, region, jsonMapper, timeout); } @Override diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingModelIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingModelIT.java index 8d4d577179..45113a3e73 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingModelIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingModelIT.java @@ -19,11 +19,11 @@ import java.time.Duration; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.RequiresAwsCredentials; import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi; @@ -170,7 +170,7 @@ public static class TestConfiguration { @Bean public CohereEmbeddingBedrockApi cohereEmbeddingApi() { return new CohereEmbeddingBedrockApi(CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id(), - EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new ObjectMapper(), + EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); } diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApiIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApiIT.java index b452661844..3a59f7e42c 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApiIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/api/CohereEmbeddingBedrockApiIT.java @@ -19,10 +19,10 @@ import java.time.Duration; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.RequiresAwsCredentials; import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel; @@ -40,7 +40,7 @@ public class CohereEmbeddingBedrockApiIT { CohereEmbeddingBedrockApi api = new CohereEmbeddingBedrockApi( CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id(), EnvironmentVariableCredentialsProvider.create(), - Region.US_EAST_1.id(), new ObjectMapper(), Duration.ofMinutes(2)); + Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); @Test public void embedText() { diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingModelIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingModelIT.java index eb57c1ca31..bd213f69bf 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingModelIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingModelIT.java @@ -21,11 +21,11 @@ import java.util.Base64; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.RequiresAwsCredentials; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType; @@ -86,7 +86,7 @@ public TestObservationRegistry observationRegistry() { @Bean public TitanEmbeddingBedrockApi titanEmbeddingApi() { return new TitanEmbeddingBedrockApi(TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id(), - EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new ObjectMapper(), + EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); } diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApiIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApiIT.java index a7b55ff138..a2b9549e83 100644 --- a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApiIT.java +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/titan/api/TitanEmbeddingBedrockApiIT.java @@ -20,10 +20,10 @@ import java.time.Duration; import java.util.Base64; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.bedrock.RequiresAwsCredentials; import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel; @@ -45,7 +45,7 @@ public void embedTextV1() { TitanEmbeddingBedrockApi titanEmbedApi = new TitanEmbeddingBedrockApi( TitanEmbeddingModel.TITAN_EMBED_TEXT_V1.id(), EnvironmentVariableCredentialsProvider.create(), - Region.US_EAST_1.id(), new ObjectMapper(), Duration.ofMinutes(2)); + Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); TitanEmbeddingRequest request = TitanEmbeddingRequest.builder().inputText("I like to eat apples.").build(); @@ -61,7 +61,7 @@ public void embedTextV2() { TitanEmbeddingBedrockApi titanEmbedApi = new TitanEmbeddingBedrockApi( TitanEmbeddingModel.TITAN_EMBED_TEXT_V2.id(), EnvironmentVariableCredentialsProvider.create(), - Region.US_EAST_1.id(), new ObjectMapper(), Duration.ofMinutes(2)); + Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); TitanEmbeddingRequest request = TitanEmbeddingRequest.builder().inputText("I like to eat apples.").build(); @@ -77,7 +77,7 @@ public void embedImage() throws IOException { TitanEmbeddingBedrockApi titanEmbedApi = new TitanEmbeddingBedrockApi( TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id(), EnvironmentVariableCredentialsProvider.create(), - Region.US_EAST_1.id(), new ObjectMapper(), Duration.ofMinutes(2)); + Region.US_EAST_1.id(), new JsonMapper(), Duration.ofMinutes(2)); byte[] image = new DefaultResourceLoader().getResource("classpath:/spring_framework.png") .getContentAsByteArray(); diff --git a/models/spring-ai-elevenlabs/pom.xml b/models/spring-ai-elevenlabs/pom.xml index 9f2208ba93..811c5f9c18 100644 --- a/models/spring-ai-elevenlabs/pom.xml +++ b/models/spring-ai-elevenlabs/pom.xml @@ -76,7 +76,7 @@ - com.fasterxml.jackson.dataformat + tools.jackson.dataformat jackson-dataformat-xml test diff --git a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java index b098989316..88a1a88093 100644 --- a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java +++ b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java @@ -49,6 +49,8 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; @@ -183,6 +185,10 @@ public class GoogleGenAiChatModel implements ChatModel, DisposableBean { */ private final ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate; + private final JsonMapper jsonMapper = ModelOptionsUtils.JSON_MAPPER.rebuild() + .addMixIn(Schema.class, SchemaMixin.class) + .build(); + /** * Conventions to use for generating observations. */ @@ -259,7 +265,7 @@ private static GeminiMessageType toGeminiMessageType(@NonNull MessageType type) }; } - static List messageToGeminiParts(Message message) { + List messageToGeminiParts(Message message) { if (message instanceof SystemMessage systemMessage) { @@ -372,10 +378,10 @@ else if (data instanceof URI || data instanceof String) { } // Helper methods for JSON/Map conversion - private static Map parseJsonToMap(String json) { + private Map parseJsonToMap(String json) { try { // First, try to parse as an array - Object parsed = ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Object.class); + Object parsed = this.jsonMapper.readValue(json, Object.class); if (parsed instanceof List) { // It's an array, wrap it in a map with "result" key Map wrapper = new HashMap<>(); @@ -398,19 +404,18 @@ else if (parsed instanceof Map) { } } - private static String mapToJson(Map map) { + private String mapToJson(Map map) { try { - return ModelOptionsUtils.OBJECT_MAPPER.writeValueAsString(map); + return this.jsonMapper.writeValueAsString(map); } catch (Exception e) { throw new RuntimeException("Failed to convert map to JSON", e); } } - private static Schema jsonToSchema(String json) { + private Schema jsonToSchema(String json) { try { - // Parse JSON into Schema using OBJECT_MAPPER - return ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Schema.class); + return this.jsonMapper.readValue(json, Schema.class); } catch (Exception e) { throw new RuntimeException(e); @@ -1213,4 +1218,9 @@ public record GeminiRequest(List contents, String modelName, GenerateCo } + @JsonDeserialize(builder = Schema.Builder.class) + private static class SchemaMixin { + + } + } diff --git a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/GoogleGenAiToolCallingManager.java b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/GoogleGenAiToolCallingManager.java index eb19d56ac5..1066ced091 100644 --- a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/GoogleGenAiToolCallingManager.java +++ b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/GoogleGenAiToolCallingManager.java @@ -18,7 +18,7 @@ import java.util.List; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; diff --git a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/JsonSchemaConverter.java b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/JsonSchemaConverter.java index 0bb77e02c5..49fa1df2b9 100644 --- a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/JsonSchemaConverter.java +++ b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/schema/JsonSchemaConverter.java @@ -22,8 +22,12 @@ * @since 1.0.0 */ -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Map; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.util.json.JsonParser; import org.springframework.util.Assert; @@ -45,7 +49,7 @@ private JsonSchemaConverter() { */ public static ObjectNode fromJson(String jsonString) { try { - return (ObjectNode) JsonParser.getObjectMapper().readTree(jsonString); + return (ObjectNode) JsonParser.getJsonMapper().readTree(jsonString); } catch (Exception e) { throw new RuntimeException("Failed to parse JSON: " + jsonString, e); @@ -63,7 +67,7 @@ public static ObjectNode convertToOpenApiSchema(ObjectNode jsonSchemaNode) { try { // Convert to OpenAPI schema using our custom conversion logic - ObjectNode openApiSchema = convertSchema(jsonSchemaNode, JsonParser.getObjectMapper().getNodeFactory()); + ObjectNode openApiSchema = convertSchema(jsonSchemaNode, JsonParser.getJsonMapper().getNodeFactory()); // Add OpenAPI-specific metadata if (!openApiSchema.has("openapi")) { @@ -109,12 +113,13 @@ private static void handleJsonSchemaSpecifics(ObjectNode source, ObjectNode targ Assert.notNull(target, "Target node must not be null"); if (source.has("properties")) { ObjectNode properties = target.putObject("properties"); - source.get("properties").fields().forEachRemaining(entry -> { + var fields = source.get("properties").properties(); + for (Map.Entry entry : fields) { if (entry.getValue() instanceof ObjectNode) { - properties.set(entry.getKey(), convertSchema((ObjectNode) entry.getValue(), - JsonParser.getObjectMapper().getNodeFactory())); + properties.set(entry.getKey(), + convertSchema((ObjectNode) entry.getValue(), JsonParser.getJsonMapper().getNodeFactory())); } - }); + } } // Handle required array @@ -130,7 +135,7 @@ private static void handleJsonSchemaSpecifics(ObjectNode source, ObjectNode targ } else if (additionalProps.isObject()) { target.set("additionalProperties", - convertSchema((ObjectNode) additionalProps, JsonParser.getObjectMapper().getNodeFactory())); + convertSchema((ObjectNode) additionalProps, JsonParser.getJsonMapper().getNodeFactory())); } } @@ -138,7 +143,7 @@ else if (additionalProps.isObject()) { if (source.has("items")) { JsonNode items = source.get("items"); if (items.isObject()) { - target.set("items", convertSchema((ObjectNode) items, JsonParser.getObjectMapper().getNodeFactory())); + target.set("items", convertSchema((ObjectNode) items, JsonParser.getJsonMapper().getNodeFactory())); } } @@ -148,7 +153,7 @@ else if (additionalProps.isObject()) { if (source.has(combiner)) { JsonNode combinerNode = source.get(combiner); if (combinerNode.isArray()) { - target.putArray(combiner).addAll((com.fasterxml.jackson.databind.node.ArrayNode) combinerNode); + target.putArray(combiner).addAll((ArrayNode) combinerNode); } } } @@ -160,8 +165,7 @@ else if (additionalProps.isObject()) { * @param factory The JsonNodeFactory to create new nodes * @return The converted OpenAPI schema as ObjectNode */ - private static ObjectNode convertSchema(ObjectNode source, - com.fasterxml.jackson.databind.node.JsonNodeFactory factory) { + private static ObjectNode convertSchema(ObjectNode source, JsonNodeFactory factory) { Assert.notNull(source, "Source node must not be null"); Assert.notNull(factory, "JsonNodeFactory must not be null"); diff --git a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/metadata/GoogleGenAiUsageTests.java b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/metadata/GoogleGenAiUsageTests.java index 37bf1380fa..8ee08d5d62 100644 --- a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/metadata/GoogleGenAiUsageTests.java +++ b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/metadata/GoogleGenAiUsageTests.java @@ -18,12 +18,12 @@ import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.genai.types.GenerateContentResponseUsageMetadata; import com.google.genai.types.MediaModality; import com.google.genai.types.ModalityTokenCount; import com.google.genai.types.TrafficType; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; @@ -35,8 +35,6 @@ */ public class GoogleGenAiUsageTests { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Test void testBasicUsageExtraction() { // Create mock usage metadata @@ -239,7 +237,7 @@ void testJsonSerialization() throws Exception { GoogleGenAiUsage usage = new GoogleGenAiUsage(100, 50, 175, 25, 30, 15, null, null, null, null, GoogleGenAiTrafficType.ON_DEMAND, null); - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).contains("\"promptTokens\":100"); assertThat(json).contains("\"completionTokens\":50"); diff --git a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/schema/JsonSchemaConverterTests.java b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/schema/JsonSchemaConverterTests.java index 19ba4e3b66..1b3cdf3e7f 100644 --- a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/schema/JsonSchemaConverterTests.java +++ b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/schema/JsonSchemaConverterTests.java @@ -16,9 +16,9 @@ package org.springframework.ai.google.genai.schema; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ObjectNode; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/models/spring-ai-huggingface/pom.xml b/models/spring-ai-huggingface/pom.xml index c4b390d16f..73d1a117d4 100644 --- a/models/spring-ai-huggingface/pom.xml +++ b/models/spring-ai-huggingface/pom.xml @@ -60,6 +60,11 @@ 1.3.2 + + com.fasterxml.jackson.core + jackson-databind + + diff --git a/models/spring-ai-huggingface/src/main/java/org/springframework/ai/huggingface/HuggingfaceChatModel.java b/models/spring-ai-huggingface/src/main/java/org/springframework/ai/huggingface/HuggingfaceChatModel.java index 3892ca372f..67b1c89785 100644 --- a/models/spring-ai-huggingface/src/main/java/org/springframework/ai/huggingface/HuggingfaceChatModel.java +++ b/models/spring-ai-huggingface/src/main/java/org/springframework/ai/huggingface/HuggingfaceChatModel.java @@ -20,8 +20,8 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.ChatModel; @@ -51,7 +51,7 @@ public class HuggingfaceChatModel implements ChatModel { /** * Mapper for converting between Java objects and JSON. */ - private final ObjectMapper objectMapper = new ObjectMapper(); + private final JsonMapper jsonMapper = new JsonMapper(); /** * API for text generation inferences. @@ -97,10 +97,10 @@ public ChatResponse call(Prompt prompt) { for (GenerateResponse generateResponse : generateResponses) { String generatedText = generateResponse.getGeneratedText(); AllOfGenerateResponseDetails allOfGenerateResponseDetails = generateResponse.getDetails(); - Map detailsMap = this.objectMapper.convertValue(allOfGenerateResponseDetails, - new TypeReference<>() { + Map detailsMap = JsonMapper.shared() + .convertValue(allOfGenerateResponseDetails, new TypeReference<>() { - }); + }); Generation generation = new Generation( AssistantMessage.builder().content(generatedText).properties(detailsMap).build()); generations.add(generation); diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java index 86c337a58e..d42a86022e 100644 --- a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java +++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java @@ -20,12 +20,11 @@ import java.util.List; import java.util.Objects; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletion; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage; @@ -50,12 +49,7 @@ public class MiniMaxApiToolFunctionCallIT { MiniMaxApi miniMaxApi = new MiniMaxApi(System.getenv("MINIMAX_API_KEY")); private static T fromJson(String json, Class targetClass) { - try { - return new ObjectMapper().readValue(json, targetClass); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return JsonMapper.shared().readValue(json, targetClass); } @SuppressWarnings("null") diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatOptionsTests.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatOptionsTests.java index 65cfd44ab9..fe3a3278c4 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatOptionsTests.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatOptionsTests.java @@ -20,9 +20,8 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ResponseFormat; @@ -452,11 +451,11 @@ void testBuilderOutputSchema() { } @Test - void testJsonSerializationOfResponseFormat() throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); + void testJsonSerializationOfResponseFormat() { + JsonMapper jsonMapper = new JsonMapper(); ResponseFormat format = ResponseFormat.jsonSchema(Map.of("type", "object")); - String json = objectMapper.writeValueAsString(format); + String json = jsonMapper.writeValueAsString(format); assertThat(json).contains("\"type\":\"json_schema\""); assertThat(json).contains("\"json_schema\""); diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/MistralAiApiToolFunctionCallIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/MistralAiApiToolFunctionCallIT.java index ef405c25de..281d2a0387 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/MistralAiApiToolFunctionCallIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/MistralAiApiToolFunctionCallIT.java @@ -19,12 +19,11 @@ import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletion; @@ -57,17 +56,12 @@ public class MistralAiApiToolFunctionCallIT { MistralAiApi completionApi = MistralAiApi.builder().apiKey(System.getenv("MISTRAL_AI_API_KEY")).build(); private static T fromJson(String json, Class targetClass) { - try { - return new ObjectMapper().readValue(json, targetClass); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return JsonMapper.shared().readValue(json, targetClass); } @Test @SuppressWarnings("null") - public void toolFunctionCall() throws JsonProcessingException { + public void toolFunctionCall() { // Step 1: send the conversation and available functions to the model var message = new ChatCompletionMessage( @@ -108,9 +102,6 @@ public void toolFunctionCall() throws JsonProcessingException { ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(messages, MISTRAL_AI_CHAT_MODEL, List.of(functionTool), ToolChoice.AUTO); - System.out - .println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(chatCompletionRequest)); - ResponseEntity chatCompletion = this.completionApi.chatCompletionEntity(chatCompletionRequest); assertThat(chatCompletion.getBody()).isNotNull(); diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/PaymentStatusFunctionCallingIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/PaymentStatusFunctionCallingIT.java index a207af4a1f..4e4dd0f3a6 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/PaymentStatusFunctionCallingIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/api/tool/PaymentStatusFunctionCallingIT.java @@ -22,12 +22,11 @@ import java.util.function.Function; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletion; @@ -68,12 +67,7 @@ public class PaymentStatusFunctionCallingIT { private final Logger logger = LoggerFactory.getLogger(PaymentStatusFunctionCallingIT.class); private static T jsonToObject(String json, Class targetClass) { - try { - return new ObjectMapper().readValue(json, targetClass); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return JsonMapper.shared().readValue(json, targetClass); } @Test diff --git a/models/spring-ai-ollama/pom.xml b/models/spring-ai-ollama/pom.xml index 595ec72377..4524e8b924 100644 --- a/models/spring-ai-ollama/pom.xml +++ b/models/spring-ai-ollama/pom.xml @@ -63,15 +63,10 @@ - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - org.slf4j slf4j-api diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java index d36bf43f6c..8b641b46ff 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Optional; -import com.fasterxml.jackson.core.type.TypeReference; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; @@ -32,6 +31,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; +import tools.jackson.core.type.TypeReference; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.MessageType; diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/ThinkOption.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/ThinkOption.java index 3bc6e8ec14..f9440856cb 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/ThinkOption.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/ThinkOption.java @@ -16,19 +16,19 @@ package org.springframework.ai.ollama.api; -import java.io.IOException; import java.util.List; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; /** * Represents the thinking option for Ollama models. The think option controls whether @@ -55,15 +55,15 @@ public sealed interface ThinkOption { /** * Serializer that writes ThinkOption as raw boolean or string values. */ - class ThinkOptionSerializer extends JsonSerializer { + class ThinkOptionSerializer extends ValueSerializer { @Override - public void serialize(ThinkOption value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + public void serialize(ThinkOption value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException { if (value == null) { gen.writeNull(); } else { - gen.writeObject(value.toJsonValue()); + gen.writePOJO(value.toJsonValue()); } } @@ -72,10 +72,10 @@ public void serialize(ThinkOption value, JsonGenerator gen, SerializerProvider s /** * Deserializer that reads boolean or string values into ThinkOption instances. */ - class ThinkOptionDeserializer extends JsonDeserializer { + class ThinkOptionDeserializer extends ValueDeserializer { @Override - public @Nullable ThinkOption deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + public @Nullable ThinkOption deserialize(JsonParser p, DeserializationContext ctxt) { JsonToken token = p.currentToken(); if (token == JsonToken.VALUE_TRUE) { return ThinkBoolean.ENABLED; @@ -89,7 +89,7 @@ else if (token == JsonToken.VALUE_STRING) { else if (token == JsonToken.VALUE_NULL) { return null; } - throw new IOException("Cannot deserialize ThinkOption from token: " + token); + throw new IllegalStateException("Cannot deserialize ThinkOption from token: " + token); } } diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaChatOptionsTests.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaChatOptionsTests.java index fb6bbf1697..f8cb960703 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaChatOptionsTests.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaChatOptionsTests.java @@ -21,8 +21,8 @@ import java.util.Map; import java.util.Set; -import com.fasterxml.jackson.core.JsonParseException; import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; import org.springframework.ai.util.ResourceUtils; @@ -139,8 +139,7 @@ void testOutputSchemaOptionWithJsonSchemaObjectAsString() { @Test void testOutputSchemaOptionWithJsonAsString() { - assertThatThrownBy(() -> OllamaChatOptions.builder().outputSchema("json")) - .hasCauseInstanceOf(JsonParseException.class) + assertThatThrownBy(() -> OllamaChatOptions.builder().outputSchema("json")).isInstanceOf(JacksonException.class) .hasMessageContaining("Unrecognized token 'json'"); } diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaDurationFieldsTests.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaDurationFieldsTests.java index aa08c54a60..6b75289bfc 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaDurationFieldsTests.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/OllamaDurationFieldsTests.java @@ -16,8 +16,6 @@ package org.springframework.ai.ollama.api; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import org.junit.jupiter.api.Test; import org.springframework.ai.model.ModelOptionsUtils; @@ -31,7 +29,7 @@ public class OllamaDurationFieldsTests { @Test - public void testDurationFields() throws JsonMappingException, JsonProcessingException { + public void testDurationFields() { var value = ModelOptionsUtils.jsonToObject(""" { diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/ThinkOptionTests.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/ThinkOptionTests.java index 68964d41c9..d7ab412ff7 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/ThinkOptionTests.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/api/ThinkOptionTests.java @@ -16,8 +16,8 @@ package org.springframework.ai.ollama.api; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,89 +29,87 @@ */ class ThinkOptionTests { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Test - void testThinkBooleanEnabledSerialization() throws Exception { + void testThinkBooleanEnabledSerialization() { ThinkOption option = ThinkOption.ThinkBoolean.ENABLED; - String json = this.objectMapper.writeValueAsString(option); + String json = JsonMapper.shared().writeValueAsString(option); assertThat(json).isEqualTo("true"); } @Test - void testThinkBooleanDisabledSerialization() throws Exception { + void testThinkBooleanDisabledSerialization() { ThinkOption option = ThinkOption.ThinkBoolean.DISABLED; - String json = this.objectMapper.writeValueAsString(option); + String json = JsonMapper.shared().writeValueAsString(option); assertThat(json).isEqualTo("false"); } @Test - void testThinkLevelLowSerialization() throws Exception { + void testThinkLevelLowSerialization() { ThinkOption option = ThinkOption.ThinkLevel.LOW; - String json = this.objectMapper.writeValueAsString(option); + String json = JsonMapper.shared().writeValueAsString(option); assertThat(json).isEqualTo("\"low\""); } @Test - void testThinkLevelMediumSerialization() throws Exception { + void testThinkLevelMediumSerialization() { ThinkOption option = ThinkOption.ThinkLevel.MEDIUM; - String json = this.objectMapper.writeValueAsString(option); + String json = JsonMapper.shared().writeValueAsString(option); assertThat(json).isEqualTo("\"medium\""); } @Test void testThinkLevelHighSerialization() throws Exception { ThinkOption option = ThinkOption.ThinkLevel.HIGH; - String json = this.objectMapper.writeValueAsString(option); + String json = JsonMapper.shared().writeValueAsString(option); assertThat(json).isEqualTo("\"high\""); } @Test - void testDeserializeBooleanTrue() throws Exception { + void testDeserializeBooleanTrue() { String json = "true"; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isEqualTo(ThinkOption.ThinkBoolean.ENABLED); assertThat(option).isInstanceOf(ThinkOption.ThinkBoolean.class); assertThat(((ThinkOption.ThinkBoolean) option).enabled()).isTrue(); } @Test - void testDeserializeBooleanFalse() throws Exception { + void testDeserializeBooleanFalse() { String json = "false"; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isEqualTo(ThinkOption.ThinkBoolean.DISABLED); assertThat(option).isInstanceOf(ThinkOption.ThinkBoolean.class); assertThat(((ThinkOption.ThinkBoolean) option).enabled()).isFalse(); } @Test - void testDeserializeStringLow() throws Exception { + void testDeserializeStringLow() { String json = "\"low\""; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isInstanceOf(ThinkOption.ThinkLevel.class); assertThat(((ThinkOption.ThinkLevel) option).level()).isEqualTo("low"); } @Test - void testDeserializeStringMedium() throws Exception { + void testDeserializeStringMedium() { String json = "\"medium\""; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isInstanceOf(ThinkOption.ThinkLevel.class); assertThat(((ThinkOption.ThinkLevel) option).level()).isEqualTo("medium"); } @Test - void testDeserializeStringHigh() throws Exception { + void testDeserializeStringHigh() { String json = "\"high\""; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isInstanceOf(ThinkOption.ThinkLevel.class); assertThat(((ThinkOption.ThinkLevel) option).level()).isEqualTo("high"); } @Test - void testDeserializeNull() throws Exception { + void testDeserializeNull() { String json = "null"; - ThinkOption option = this.objectMapper.readValue(json, ThinkOption.class); + ThinkOption option = JsonMapper.shared().readValue(json, ThinkOption.class); assertThat(option).isNull(); } diff --git a/models/spring-ai-openai-sdk/src/main/java/org/springframework/ai/openaisdk/OpenAiSdkChatModel.java b/models/spring-ai-openai-sdk/src/main/java/org/springframework/ai/openaisdk/OpenAiSdkChatModel.java index 46db737886..35f03d5469 100644 --- a/models/spring-ai-openai-sdk/src/main/java/org/springframework/ai/openaisdk/OpenAiSdkChatModel.java +++ b/models/spring-ai-openai-sdk/src/main/java/org/springframework/ai/openaisdk/OpenAiSdkChatModel.java @@ -26,7 +26,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.JsonNode; import com.openai.client.OpenAIClient; import com.openai.client.OpenAIClientAsync; import com.openai.core.JsonValue; @@ -63,6 +62,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; +import tools.jackson.databind.JsonNode; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.MessageType; @@ -1091,7 +1091,7 @@ else if (responseFormat.getType().equals(ResponseFormat.Type.JSON_SCHEMA)) { } else if (requestOptions.getToolChoice() instanceof String json) { try { - var node = ModelOptionsUtils.OBJECT_MAPPER.readTree(json); + var node = ModelOptionsUtils.JSON_MAPPER.readTree(json); builder.toolChoice(parseToolChoice(node)); } catch (Exception e) { diff --git a/models/spring-ai-openai-sdk/src/test/java/org/springframework/ai/openaisdk/chat/OpenAiSdkChatModelResponseFormatIT.java b/models/spring-ai-openai-sdk/src/test/java/org/springframework/ai/openaisdk/chat/OpenAiSdkChatModelResponseFormatIT.java index 86ac56058a..9ee45a0acf 100644 --- a/models/spring-ai-openai-sdk/src/test/java/org/springframework/ai/openaisdk/chat/OpenAiSdkChatModelResponseFormatIT.java +++ b/models/spring-ai-openai-sdk/src/test/java/org/springframework/ai/openaisdk/chat/OpenAiSdkChatModelResponseFormatIT.java @@ -18,14 +18,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -47,8 +46,9 @@ @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") public class OpenAiSdkChatModelResponseFormatIT { - private static final ObjectMapper MAPPER = new ObjectMapper() - .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + private static final JsonMapper jsonMapper = JsonMapper.builder() + .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -57,7 +57,7 @@ public class OpenAiSdkChatModelResponseFormatIT { public static boolean isValidJson(String json) { try { - MAPPER.readTree(json); + jsonMapper.readTree(json); } catch (JacksonException e) { return false; @@ -133,7 +133,7 @@ void jsonSchema() { } @Test - void jsonSchemaThroughIndividualSetters() throws JsonProcessingException { + void jsonSchemaThroughIndividualSetters() { var jsonSchema = """ { diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index 92b16f85e4..6965097d57 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -33,9 +33,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import tools.jackson.databind.annotation.JsonDeserialize; import org.springframework.ai.model.ApiKey; import org.springframework.ai.model.ChatModelDescription; diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializer.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializer.java index 9eafcc1404..6b4c18af43 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializer.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializer.java @@ -16,16 +16,16 @@ package org.springframework.ai.openai.api; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Base64; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; /** * Used to deserialize the `embedding` field returned by the model. @@ -40,11 +40,11 @@ * * @author Sun Yuhan */ -public class OpenAiEmbeddingDeserializer extends JsonDeserializer { +public class OpenAiEmbeddingDeserializer extends ValueDeserializer { @Override public float[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException, JacksonException { + throws JacksonException { JsonToken token = jsonParser.currentToken(); if (token == JsonToken.START_ARRAY) { return jsonParser.readValueAs(float[].class); @@ -65,7 +65,7 @@ else if (token == JsonToken.VALUE_STRING) { return embeddingArray; } else { - throw new IOException("Illegal embedding: " + token); + throw new StreamReadException(jsonParser, "Illegal embedding: " + token); } } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/ExtraBodySerializationTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/ExtraBodySerializationTest.java index 32ae9ab075..1f3dee6d64 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/ExtraBodySerializationTest.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/ExtraBodySerializationTest.java @@ -19,8 +19,8 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.openai.OpenAiChatOptions; @@ -36,8 +36,6 @@ */ class ExtraBodySerializationTest { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Test void testExtraBodySerializationFlattensToTopLevel() throws Exception { // Arrange: Create request with extraBody containing vLLM/Ollama parameters @@ -49,7 +47,7 @@ void testExtraBodySerializationFlattensToTopLevel() throws Exception { ); // Act: Serialize to JSON - String json = this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); + String json = JsonMapper.shared().writerWithDefaultPrettyPrinter().writeValueAsString(request); // Assert: Verify @JsonAnyGetter flattens fields to top level assertThat(json).contains("\"top_k\" : 50"); @@ -68,7 +66,7 @@ void testExtraBodyWithEmptyMap() throws Exception { ); // Act - String json = this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); + String json = JsonMapper.shared().writerWithDefaultPrettyPrinter().writeValueAsString(request); // Assert: No extra fields should appear assertThat(json).doesNotContain("extra_body"); @@ -87,7 +85,7 @@ void testExtraBodyNullSerialization() throws Exception { ); // Act - String json = this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); + String json = JsonMapper.shared().writerWithDefaultPrettyPrinter().writeValueAsString(request); // Assert: extra_body should not appear in JSON when null assertThat(json).doesNotContain("extra_body"); @@ -109,7 +107,7 @@ void testExtraBodyDeserialization() throws Exception { """; // Act: Deserialize JSON to ChatCompletionRequest - ChatCompletionRequest request = this.objectMapper.readValue(json, ChatCompletionRequest.class); + ChatCompletionRequest request = JsonMapper.shared().readValue(json, ChatCompletionRequest.class); // Assert: Extra fields should be captured in extraBody map assertThat(request.extraBody()).isNotNull(); @@ -134,10 +132,10 @@ void testRoundTripSerializationDeserialization() throws Exception { ); // Act: Serialize to JSON - String json = this.objectMapper.writeValueAsString(originalRequest); + String json = JsonMapper.shared().writeValueAsString(originalRequest); // Act: Deserialize back to object - ChatCompletionRequest deserializedRequest = this.objectMapper.readValue(json, ChatCompletionRequest.class); + ChatCompletionRequest deserializedRequest = JsonMapper.shared().readValue(json, ChatCompletionRequest.class); // Assert: All extraBody fields should survive round trip assertThat(deserializedRequest.extraBody()).isNotNull(); @@ -163,7 +161,7 @@ void testDeserializationWithNullExtraBody() throws Exception { """; // Act: Deserialize - ChatCompletionRequest request = this.objectMapper.readValue(json, ChatCompletionRequest.class); + ChatCompletionRequest request = JsonMapper.shared().readValue(json, ChatCompletionRequest.class); // Assert: extraBody should be null or empty when no extra fields present // (depending on Jackson configuration and constructor behavior) @@ -194,7 +192,7 @@ void testDeserializationWithComplexExtraFields() throws Exception { """; // Act: Deserialize - ChatCompletionRequest request = this.objectMapper.readValue(json, ChatCompletionRequest.class); + ChatCompletionRequest request = JsonMapper.shared().readValue(json, ChatCompletionRequest.class); // Assert: Real vLLM extra fields should be captured assertThat(request.extraBody()).isNotNull(); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializerTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializerTests.java index 4ea15df55a..6abb6cdd3c 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializerTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiEmbeddingDeserializerTests.java @@ -16,17 +16,16 @@ package org.springframework.ai.openai.api; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Base64; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.json.JsonMapper; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,10 +43,8 @@ class OpenAiEmbeddingDeserializerTests { private final OpenAiEmbeddingDeserializer deserializer = new OpenAiEmbeddingDeserializer(); - private final ObjectMapper mapper = new ObjectMapper(); - @Test - void testDeserializeFloatArray() throws Exception { + void testDeserializeFloatArray() { JsonParser parser = mock(JsonParser.class); DeserializationContext context = mock(DeserializationContext.class); @@ -60,7 +57,7 @@ void testDeserializeFloatArray() throws Exception { } @Test - void testDeserializeBase64String() throws Exception { + void testDeserializeBase64String() { float[] original = new float[] { 4.2f, -1.5f, 0.0f }; ByteBuffer buffer = ByteBuffer.allocate(original.length * Float.BYTES); buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -87,12 +84,12 @@ void testDeserializeIllegalToken() { when(parser.currentToken()).thenReturn(JsonToken.VALUE_NUMBER_INT); - IOException e = assertThrows(IOException.class, () -> this.deserializer.deserialize(parser, context)); + JacksonException e = assertThrows(JacksonException.class, () -> this.deserializer.deserialize(parser, context)); assertTrue(e.getMessage().contains("Illegal embedding")); } @Test - void testDeserializeEmbeddingWithFloatArray() throws Exception { + void testDeserializeEmbeddingWithFloatArray() { String json = """ { "index": 1, @@ -100,14 +97,14 @@ void testDeserializeEmbeddingWithFloatArray() throws Exception { "object": "embedding" } """; - OpenAiApi.Embedding embedding = this.mapper.readValue(json, OpenAiApi.Embedding.class); + OpenAiApi.Embedding embedding = JsonMapper.shared().readValue(json, OpenAiApi.Embedding.class); assertEquals(1, embedding.index()); assertArrayEquals(new float[] { 1.0f, 2.0f, 3.0f }, embedding.embedding(), 0.0001f); assertEquals("embedding", embedding.object()); } @Test - void testDeserializeEmbeddingWithBase64String() throws Exception { + void testDeserializeEmbeddingWithBase64String() { float[] original = new float[] { 4.2f, -1.5f, 0.0f }; ByteBuffer buffer = ByteBuffer.allocate(original.length * Float.BYTES); buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -124,7 +121,7 @@ void testDeserializeEmbeddingWithBase64String() throws Exception { } """.formatted(base64); - OpenAiApi.Embedding embedding = this.mapper.readValue(json, OpenAiApi.Embedding.class); + OpenAiApi.Embedding embedding = JsonMapper.shared().readValue(json, OpenAiApi.Embedding.class); assertEquals(2, embedding.index()); assertArrayEquals(original, embedding.embedding(), 0.0001f); assertEquals("embedding", embedding.object()); @@ -139,8 +136,8 @@ void testDeserializeEmbeddingWithWrongType() { "object": "embedding" } """; - JsonProcessingException ex = assertThrows(JsonProcessingException.class, - () -> this.mapper.readValue(json, OpenAiApi.Embedding.class)); + JacksonException ex = assertThrows(JacksonException.class, + () -> JsonMapper.shared().readValue(json, OpenAiApi.Embedding.class)); assertTrue(ex.getMessage().contains("Illegal embedding")); } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/tool/OpenAiApiToolFunctionCallIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/tool/OpenAiApiToolFunctionCallIT.java index 3ce6e3f24e..8cb1e25f5c 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/tool/OpenAiApiToolFunctionCallIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/tool/OpenAiApiToolFunctionCallIT.java @@ -19,12 +19,11 @@ import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.openai.api.OpenAiApi; @@ -51,17 +50,14 @@ public class OpenAiApiToolFunctionCallIT { private final Logger logger = LoggerFactory.getLogger(OpenAiApiToolFunctionCallIT.class); - MockWeatherService weatherService = new MockWeatherService(); + private final MockWeatherService weatherService = new MockWeatherService(); - OpenAiApi completionApi = OpenAiApi.builder().apiKey(System.getenv("OPENAI_API_KEY")).build(); + private final OpenAiApi completionApi = OpenAiApi.builder().apiKey(System.getenv("OPENAI_API_KEY")).build(); - private static T fromJson(String json, Class targetClass) { - try { - return new ObjectMapper().readValue(json, targetClass); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + private final JsonMapper jsonMapper = new JsonMapper(); + + private T fromJson(String json, Class targetClass) { + return this.jsonMapper.readValue(json, targetClass); } @SuppressWarnings("null") diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/ExtraBodyWireTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/ExtraBodyWireTest.java index 3796cbd458..ee5c92ea79 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/ExtraBodyWireTest.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/ExtraBodyWireTest.java @@ -18,14 +18,14 @@ import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; @@ -57,7 +57,7 @@ class ExtraBodyWireTest { private MockWebServer mockWebServer; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final JsonMapper jsonMapper = new JsonMapper(); @BeforeEach void setUp() throws Exception { @@ -92,7 +92,7 @@ void extraBodyFromRuntimeOptionsAppearsInHttpRequest() throws Exception { // Assert: Verify the wire-level JSON contains flattened extraBody fields RecordedRequest recordedRequest = this.mockWebServer.takeRequest(); String requestBody = recordedRequest.getBody().readUtf8(); - JsonNode json = this.objectMapper.readTree(requestBody); + JsonNode json = this.jsonMapper.readTree(requestBody); // Verify extraBody fields are at top level assertThat(json.has("top_k")).as("top_k should be at top level").isTrue(); @@ -126,7 +126,7 @@ void extraBodyFromDefaultOptionsAppearsInHttpRequest() throws Exception { // Assert: Verify wire-level JSON RecordedRequest recordedRequest = this.mockWebServer.takeRequest(); String requestBody = recordedRequest.getBody().readUtf8(); - JsonNode json = this.objectMapper.readTree(requestBody); + JsonNode json = this.jsonMapper.readTree(requestBody); assertThat(json.has("enable_thinking")).isTrue(); assertThat(json.get("enable_thinking").asBoolean()).isTrue(); @@ -162,7 +162,7 @@ void runtimeExtraBodyOverridesDefaultExtraBody() throws Exception { // Assert RecordedRequest recordedRequest = this.mockWebServer.takeRequest(); String requestBody = recordedRequest.getBody().readUtf8(); - JsonNode json = this.objectMapper.readTree(requestBody); + JsonNode json = this.jsonMapper.readTree(requestBody); // Runtime overrides default assertThat(json.get("top_k").asInt()).isEqualTo(100); @@ -196,7 +196,7 @@ void extraBodyWithVllmParameters() throws Exception { // Assert RecordedRequest recordedRequest = this.mockWebServer.takeRequest(); String requestBody = recordedRequest.getBody().readUtf8(); - JsonNode json = this.objectMapper.readTree(requestBody); + JsonNode json = this.jsonMapper.readTree(requestBody); // All vLLM parameters should be at top level assertThat(json.get("top_k").asInt()).isEqualTo(50); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java index 59653a2879..b45c3f172c 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java @@ -18,15 +18,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -51,16 +49,18 @@ @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") public class OpenAiChatModelResponseFormatIT { - private static ObjectMapper MAPPER = new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + private final JsonMapper jsonMapper = JsonMapper.builder() + .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); private final Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private OpenAiChatModel openAiChatModel; - public static boolean isValidJson(String json) { + boolean isValidJson(String json) { try { - MAPPER.readTree(json); + this.jsonMapper.readTree(json); } catch (JacksonException e) { return false; @@ -69,7 +69,7 @@ public static boolean isValidJson(String json) { } @Test - void jsonObject() throws JsonMappingException, JsonProcessingException { + void jsonObject() { // 400 - ResponseError[error=Error[message='json' is not one of ['json_object', // 'text'] - @@ -97,7 +97,7 @@ void jsonObject() throws JsonMappingException, JsonProcessingException { } @Test - void jsonSchema() throws JsonMappingException, JsonProcessingException { + void jsonSchema() { var jsonSchema = """ { @@ -140,7 +140,7 @@ void jsonSchema() throws JsonMappingException, JsonProcessingException { } @Test - void jsonSchemaThroughIndividualSetters() throws JsonMappingException, JsonProcessingException { + void jsonSchemaThroughIndividualSetters() { var jsonSchema = """ { @@ -183,7 +183,7 @@ void jsonSchemaThroughIndividualSetters() throws JsonMappingException, JsonProce } @Test - void jsonSchemaBeanConverter() throws JsonMappingException, JsonProcessingException { + void jsonSchemaBeanConverter() { @JsonPropertyOrder({ "steps", "final_answer" }) record MathReasoning(@JsonProperty(required = true, value = "steps") Steps steps, diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiStreamingFinishReasonTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiStreamingFinishReasonTests.java index 0b84bcef77..c26be7fa90 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiStreamingFinishReasonTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiStreamingFinishReasonTests.java @@ -18,7 +18,6 @@ import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -112,7 +111,7 @@ void testStreamingWithValidFinishReason() { } @Test - void testJsonDeserializationWithEmptyStringFinishReason() throws JsonProcessingException { + void testJsonDeserializationWithEmptyStringFinishReason() { // Test the specific JSON from the issue report String problematicJson = """ { @@ -147,7 +146,7 @@ void testJsonDeserializationWithEmptyStringFinishReason() throws JsonProcessingE } @Test - void testJsonDeserializationWithNullFinishReason() throws JsonProcessingException { + void testJsonDeserializationWithNullFinishReason() { // Test with null finish_reason (should work fine) String validJson = """ { diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java index 9ff5c5f2cf..023f57d146 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java @@ -24,7 +24,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.databind.JsonNode; import com.google.cloud.vertexai.VertexAI; import com.google.cloud.vertexai.api.Candidate; import com.google.cloud.vertexai.api.Candidate.FinishReason; @@ -51,6 +50,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; +import tools.jackson.databind.JsonNode; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; @@ -334,11 +334,11 @@ private static String structToJson(Struct struct) { private static Struct jsonToStruct(String json) { try { - JsonNode rootNode = ModelOptionsUtils.OBJECT_MAPPER.readTree(json); + JsonNode rootNode = ModelOptionsUtils.JSON_MAPPER.readTree(json); Struct.Builder structBuilder = Struct.newBuilder(); - if (rootNode.isTextual()) { + if (rootNode.isString()) { structBuilder.putFields("result", Value.newBuilder().setStringValue(json).build()); } else if (rootNode.isArray()) { diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java index e3ccf78a7f..1ad0fcdae4 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java @@ -29,7 +29,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.model.tool.StructuredOutputChatOptions; import org.springframework.ai.model.tool.ToolCallingChatOptions; diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverter.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverter.java index 0317922cbc..74276c7a8b 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverter.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverter.java @@ -21,8 +21,12 @@ * @since 1.0.0 */ -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Map; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.util.json.JsonParser; import org.springframework.util.Assert; @@ -44,7 +48,7 @@ private JsonSchemaConverter() { */ public static ObjectNode fromJson(String jsonString) { try { - return (ObjectNode) JsonParser.getObjectMapper().readTree(jsonString); + return (ObjectNode) JsonParser.getJsonMapper().readTree(jsonString); } catch (Exception e) { throw new RuntimeException("Failed to parse JSON: " + jsonString, e); @@ -62,7 +66,7 @@ public static ObjectNode convertToOpenApiSchema(ObjectNode jsonSchemaNode) { try { // Convert to OpenAPI schema using our custom conversion logic - ObjectNode openApiSchema = convertSchema(jsonSchemaNode, JsonParser.getObjectMapper().getNodeFactory()); + ObjectNode openApiSchema = convertSchema(jsonSchemaNode, JsonParser.getJsonMapper().getNodeFactory()); // Add OpenAPI-specific metadata if (!openApiSchema.has("openapi")) { @@ -108,12 +112,13 @@ private static void handleJsonSchemaSpecifics(ObjectNode source, ObjectNode targ Assert.notNull(target, "Target node must not be null"); if (source.has("properties")) { ObjectNode properties = target.putObject("properties"); - source.get("properties").fields().forEachRemaining(entry -> { + var fields = source.get("properties").properties(); + for (Map.Entry entry : fields) { if (entry.getValue() instanceof ObjectNode) { - properties.set(entry.getKey(), convertSchema((ObjectNode) entry.getValue(), - JsonParser.getObjectMapper().getNodeFactory())); + properties.set(entry.getKey(), + convertSchema((ObjectNode) entry.getValue(), JsonParser.getJsonMapper().getNodeFactory())); } - }); + } } // Handle required array @@ -129,7 +134,7 @@ private static void handleJsonSchemaSpecifics(ObjectNode source, ObjectNode targ } else if (additionalProps.isObject()) { target.set("additionalProperties", - convertSchema((ObjectNode) additionalProps, JsonParser.getObjectMapper().getNodeFactory())); + convertSchema((ObjectNode) additionalProps, JsonParser.getJsonMapper().getNodeFactory())); } } @@ -137,7 +142,7 @@ else if (additionalProps.isObject()) { if (source.has("items")) { JsonNode items = source.get("items"); if (items.isObject()) { - target.set("items", convertSchema((ObjectNode) items, JsonParser.getObjectMapper().getNodeFactory())); + target.set("items", convertSchema((ObjectNode) items, JsonParser.getJsonMapper().getNodeFactory())); } } @@ -147,7 +152,7 @@ else if (additionalProps.isObject()) { if (source.has(combiner)) { JsonNode combinerNode = source.get(combiner); if (combinerNode.isArray()) { - target.putArray(combiner).addAll((com.fasterxml.jackson.databind.node.ArrayNode) combinerNode); + target.putArray(combiner).addAll((ArrayNode) combinerNode); } } } @@ -159,8 +164,7 @@ else if (additionalProps.isObject()) { * @param factory The JsonNodeFactory to create new nodes * @return The converted OpenAPI schema as ObjectNode */ - private static ObjectNode convertSchema(ObjectNode source, - com.fasterxml.jackson.databind.node.JsonNodeFactory factory) { + private static ObjectNode convertSchema(ObjectNode source, JsonNodeFactory factory) { Assert.notNull(source, "Source node must not be null"); Assert.notNull(factory, "JsonNodeFactory must not be null"); diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/VertexToolCallingManager.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/VertexToolCallingManager.java index bd8924dd8c..dff0c6842d 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/VertexToolCallingManager.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/schema/VertexToolCallingManager.java @@ -18,7 +18,7 @@ import java.util.List; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java index e0c5aea6c1..3b1d0f80f3 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java @@ -24,13 +24,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.cloud.vertexai.Transport; import com.google.cloud.vertexai.VertexAI; import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.chat.client.AdvisorParams; import org.springframework.ai.chat.client.ChatClient; diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverterTests.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverterTests.java index 3e49663f56..b2bd525c7b 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverterTests.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/schema/JsonSchemaConverterTests.java @@ -16,9 +16,9 @@ package org.springframework.ai.vertexai.gemini.schema; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ObjectNode; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiApiToolFunctionCallIT.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiApiToolFunctionCallIT.java index 4d154750f1..97115b9118 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiApiToolFunctionCallIT.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/ZhiPuAiApiToolFunctionCallIT.java @@ -20,12 +20,11 @@ import java.util.List; import java.util.Objects; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion; @@ -46,17 +45,12 @@ public class ZhiPuAiApiToolFunctionCallIT { private final Logger logger = LoggerFactory.getLogger(ZhiPuAiApiToolFunctionCallIT.class); - MockWeatherService weatherService = new MockWeatherService(); + private final MockWeatherService weatherService = new MockWeatherService(); - ZhiPuAiApi zhiPuAiApi = ZhiPuAiApi.builder().apiKey(System.getenv("ZHIPU_AI_API_KEY")).build(); + private final ZhiPuAiApi zhiPuAiApi = ZhiPuAiApi.builder().apiKey(System.getenv("ZHIPU_AI_API_KEY")).build(); - private static T fromJson(String json, Class targetClass) { - try { - return new ObjectMapper().readValue(json, targetClass); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + private T fromJson(String json, Class targetClass) { + return JsonMapper.shared().readValue(json, targetClass); } @SuppressWarnings("null") diff --git a/pom.xml b/pom.xml index fe96372e36..39bad7aa69 100644 --- a/pom.xml +++ b/pom.xml @@ -306,7 +306,7 @@ 26.72.0 1.37.0 9.20.0 - 4.38.0 + 5.0.0 2.2.38 1.13.13 2.0.3 @@ -350,11 +350,13 @@ 4.12.0 5.5.6 - 4.1.0 + 5.1.0 - 0.17.1 - 0.8.0 + + 0.18.0-SNAPSHOT + + 0.9.0-SNAPSHOT 4.13.1 diff --git a/spring-ai-client-chat/pom.xml b/spring-ai-client-chat/pom.xml index f7a26851e5..b8bf986d09 100644 --- a/spring-ai-client-chat/pom.xml +++ b/spring-ai-client-chat/pom.xml @@ -44,17 +44,13 @@ ${project.version} + io.modelcontextprotocol.sdk - mcp-json-jackson2 + mcp-json-jackson3 ${mcp.sdk.version} - - com.fasterxml.jackson.module - jackson-module-jsonSchema - - io.swagger.core.v3 swagger-annotations-jakarta @@ -117,7 +113,7 @@ - com.fasterxml.jackson.module + tools.jackson.module jackson-module-kotlin test diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java index ec93b6e524..12da309d34 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java @@ -20,16 +20,16 @@ import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.json.TypeRef; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse; -import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator; +import io.modelcontextprotocol.json.schema.jackson3.DefaultJsonSchemaValidator; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; @@ -86,26 +86,26 @@ public final class StructuredOutputValidationAdvisor implements CallAdvisor, Str private final int maxRepeatAttempts; private StructuredOutputValidationAdvisor(int advisorOrder, Type outputType, int maxRepeatAttempts, - ObjectMapper objectMapper) { + JsonMapper jsonMapper) { Assert.notNull(advisorOrder, "advisorOrder must not be null"); Assert.notNull(outputType, "outputType must not be null"); Assert.isTrue(advisorOrder > BaseAdvisor.HIGHEST_PRECEDENCE && advisorOrder < BaseAdvisor.LOWEST_PRECEDENCE, "advisorOrder must be between HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE"); Assert.isTrue(maxRepeatAttempts >= 0, "repeatAttempts must be greater than or equal to 0"); - Assert.notNull(objectMapper, "objectMapper must not be null"); + Assert.notNull(jsonMapper, "jsonMapper must not be null"); this.advisorOrder = advisorOrder; - this.jsonvalidator = new DefaultJsonSchemaValidator(objectMapper); + this.jsonvalidator = new DefaultJsonSchemaValidator(jsonMapper); String jsonSchemaText = JsonSchemaGenerator.generateForType(outputType); logger.info("Generated JSON Schema:\n" + jsonSchemaText); - var jsonMapper = new JacksonMcpJsonMapper(JsonParser.getObjectMapper()); + var mcpJsonMapper = new JacksonMcpJsonMapper(jsonMapper); try { - this.jsonSchema = jsonMapper.readValue(jsonSchemaText, MAP_TYPE_REF); + this.jsonSchema = mcpJsonMapper.readValue(jsonSchemaText, MAP_TYPE_REF); } catch (Exception e) { throw new IllegalArgumentException("Failed to parse JSON schema", e); @@ -237,7 +237,7 @@ public final static class Builder { private int maxRepeatAttempts = 3; - private ObjectMapper objectMapper = JsonParser.getObjectMapper(); + private JsonMapper jsonMapper = JsonParser.getJsonMapper(); private Builder() { } @@ -306,12 +306,12 @@ public Builder maxRepeatAttempts(int repeatAttempts) { } /** - * Sets the ObjectMapper to be used for JSON processing. - * @param objectMapper the ObjectMapper + * Sets the JsonMapper to be used for JSON processing. + * @param jsonMapper the JsonMapper * @return this builder */ - public Builder objectMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + public Builder jsonMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; return this; } @@ -325,7 +325,7 @@ public StructuredOutputValidationAdvisor build() { throw new IllegalArgumentException("outputType must be set"); } return new StructuredOutputValidationAdvisor(this.advisorOrder, this.outputType, this.maxRepeatAttempts, - this.objectMapper); + this.jsonMapper); } } diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java index 994eb7277c..ad52875f89 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java @@ -18,7 +18,6 @@ import java.util.List; -import com.fasterxml.jackson.core.type.TypeReference; import io.micrometer.observation.ObservationRegistry; import io.modelcontextprotocol.json.TypeRef; import org.junit.jupiter.api.Test; @@ -26,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; +import tools.jackson.core.type.TypeReference; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; diff --git a/spring-ai-commons/pom.xml b/spring-ai-commons/pom.xml index 3c8c49eb0e..614f79d249 100644 --- a/spring-ai-commons/pom.xml +++ b/spring-ai-commons/pom.xml @@ -66,12 +66,7 @@ - com.fasterxml.jackson.module - jackson-module-jsonSchema - - - - com.fasterxml.jackson.core + tools.jackson.core jackson-databind @@ -102,15 +97,11 @@ - com.fasterxml.jackson.module + tools.jackson.module jackson-module-kotlin test - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - test - + diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/reader/JsonReader.java b/spring-ai-commons/src/main/java/org/springframework/ai/reader/JsonReader.java index 680798209e..b5be98a1cf 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/reader/JsonReader.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/reader/JsonReader.java @@ -23,9 +23,9 @@ import java.util.Objects; import java.util.stream.StreamSupport; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentReader; @@ -46,8 +46,6 @@ public class JsonReader implements DocumentReader { private final JsonMetadataGenerator jsonMetadataGenerator; - private final ObjectMapper objectMapper = new ObjectMapper(); - /** * The key from the JSON that we will use as the text to parse into the Document text */ @@ -73,15 +71,15 @@ public JsonReader(Resource resource, JsonMetadataGenerator jsonMetadataGenerator @Override public List get() { try { - JsonNode rootNode = this.objectMapper.readTree(this.resource.getInputStream()); + JsonNode rootNode = JsonMapper.shared().readTree(this.resource.getInputStream()); if (rootNode.isArray()) { return StreamSupport.stream(rootNode.spliterator(), true) - .map(jsonNode -> parseJsonNode(jsonNode, this.objectMapper)) + .map(jsonNode -> parseJsonNode(jsonNode, JsonMapper.shared())) .toList(); } else { - return Collections.singletonList(parseJsonNode(rootNode, this.objectMapper)); + return Collections.singletonList(parseJsonNode(rootNode, JsonMapper.shared())); } } catch (IOException e) { @@ -89,8 +87,8 @@ public List get() { } } - private Document parseJsonNode(JsonNode jsonNode, ObjectMapper objectMapper) { - Map item = objectMapper.convertValue(jsonNode, new TypeReference<>() { + private Document parseJsonNode(JsonNode jsonNode, JsonMapper jsonMapper) { + Map item = jsonMapper.convertValue(jsonNode, new TypeReference<>() { }); var sb = new StringBuilder(); @@ -107,11 +105,11 @@ private Document parseJsonNode(JsonNode jsonNode, ObjectMapper objectMapper) { protected List get(JsonNode rootNode) { if (rootNode.isArray()) { return StreamSupport.stream(rootNode.spliterator(), true) - .map(jsonNode -> parseJsonNode(jsonNode, this.objectMapper)) + .map(jsonNode -> parseJsonNode(jsonNode, JsonMapper.shared())) .toList(); } else { - return Collections.singletonList(parseJsonNode(rootNode, this.objectMapper)); + return Collections.singletonList(parseJsonNode(rootNode, JsonMapper.shared())); } } @@ -123,7 +121,7 @@ protected List get(JsonNode rootNode) { */ public List get(String pointer) { try { - JsonNode rootNode = this.objectMapper.readTree(this.resource.getInputStream()); + JsonNode rootNode = JsonMapper.shared().readTree(this.resource.getInputStream()); JsonNode targetNode = rootNode.at(pointer); if (targetNode.isMissingNode()) { diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java b/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java index 1b771dba4f..16efb53223 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java @@ -16,13 +16,10 @@ package org.springframework.ai.util; -import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.databind.Module; - -import org.springframework.beans.BeanUtils; -import org.springframework.core.KotlinDetector; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.cfg.MapperBuilder; /** * Utility methods for Jackson. @@ -32,60 +29,11 @@ public abstract class JacksonUtils { /** - * Instantiate well-known Jackson modules available in the classpath. - *

    - * Supports the following modules: Jdk8Module, - * JavaTimeModule, ParameterNamesModule and - * KotlinModule. + * Return the Jackson modules found by {@link MapperBuilder#findModules(ClassLoader)}. * @return The list of instantiated modules. */ - @SuppressWarnings("unchecked") - public static List instantiateAvailableModules() { - List modules = new ArrayList<>(); - try { - Class jdk8ModuleClass = (Class) Class - .forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module"); - com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass); - modules.add(jdk8Module); - } - catch (ClassNotFoundException ex) { - // jackson-datatype-jdk8 not available - } - - try { - Class javaTimeModuleClass = (Class) Class - .forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); - com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); - modules.add(javaTimeModule); - } - catch (ClassNotFoundException ex) { - // jackson-datatype-jsr310 not available - } - - try { - Class parameterNamesModuleClass = (Class) Class - .forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule"); - com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils - .instantiateClass(parameterNamesModuleClass); - modules.add(parameterNamesModule); - } - catch (ClassNotFoundException ex) { - // jackson-module-parameter-names not available - } - - // Kotlin present? - if (KotlinDetector.isKotlinPresent()) { - try { - Class kotlinModuleClass = (Class) Class - .forName("com.fasterxml.jackson.module.kotlin.KotlinModule"); - Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); - modules.add(kotlinModule); - } - catch (ClassNotFoundException ex) { - // jackson-module-kotlin not available - } - } - return modules; + public static List instantiateAvailableModules() { + return MapperBuilder.findModules(JacksonUtils.class.getClassLoader()); } } diff --git a/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java b/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java index 582a0a4b57..50e1ab2e26 100644 --- a/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java +++ b/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java @@ -19,9 +19,8 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +31,7 @@ class JacksonUtilsTests { * https://github.com/spring-projects/spring-ai/issues/2921 */ @Test - void usesCorrectClassLoader() throws JsonProcessingException, ClassNotFoundException { + void usesCorrectClassLoader() throws ClassNotFoundException { ClassLoader previousLoader = Thread.currentThread().getContextClassLoader(); try { // This parent CL cannot see the clazz class below. But this shouldn't matter. diff --git a/spring-ai-commons/src/test/kotlin/org/springframework/ai/utils/JacksonUtilsKotlinTests.kt b/spring-ai-commons/src/test/kotlin/org/springframework/ai/utils/JacksonUtilsKotlinTests.kt index 94ebbbb799..1269b891bd 100644 --- a/spring-ai-commons/src/test/kotlin/org/springframework/ai/utils/JacksonUtilsKotlinTests.kt +++ b/spring-ai-commons/src/test/kotlin/org/springframework/ai/utils/JacksonUtilsKotlinTests.kt @@ -16,10 +16,10 @@ package org.springframework.ai.utils -import com.fasterxml.jackson.databind.json.JsonMapper import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import org.springframework.ai.util.JacksonUtils +import tools.jackson.databind.json.JsonMapper /** * Kotlin unit tests for [JacksonUtils]. @@ -30,14 +30,14 @@ class JacksonUtilsKotlinTests { @Test fun `Deserialize to a Kotlin data class with Jackson modules detected by JacksonUtils#instantiateAvailableModules`() { - val jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build() + val jsonMapper = JsonMapper() val output = jsonMapper.readValue("{\"name\":\"Robert\",\"age\":42}", User::class.java) Assertions.assertThat(output).isEqualTo(User("Robert", 42)) } @Test fun `Serialize a Kotlin data class with Jackson modules detected by JacksonUtils#instantiateAvailableModules`() { - val jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build() + val jsonMapper = JsonMapper() val output = jsonMapper.writeValueAsString(User("Robert", 42)) Assertions.assertThat(output).isEqualTo("{\"name\":\"Robert\",\"age\":42}") } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/advisors-recursive.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/advisors-recursive.adoc index 501d2db897..f0af453052 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/advisors-recursive.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/advisors-recursive.adoc @@ -112,7 +112,7 @@ Key features: * Retries the call if validation fails, up to a configurable number of attempts * Augments the prompt with validation error messages on retry attempts to help the LLM correct its output * Uses `callAdvisorChain.copy(this)` to create a sub-chain for recursive calls -* Optionally supports a custom `ObjectMapper` for JSON processing +* Optionally supports a custom `JsonMapper` for JSON processing Example usage: diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc index 85573e0c47..d428215fc4 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc @@ -216,7 +216,7 @@ Next, create an https://github.com/spring-projects/spring-ai/blob/main/models/sp ---- var cohereEmbeddingApi =new CohereEmbeddingBedrockApi( CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V1.id(), - EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new ObjectMapper()); + EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new JsonMapper()); var embeddingModel = new BedrockCohereEmbeddingModel(this.cohereEmbeddingApi); @@ -242,7 +242,7 @@ Here is a simple snippet how to use the api programmatically: CohereEmbeddingBedrockApi api = new CohereEmbeddingBedrockApi( CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V1.id(), EnvironmentVariableCredentialsProvider.create(), - Region.US_EAST_1.id(), new ObjectMapper()); + Region.US_EAST_1.id(), new JsonMapper()); CohereEmbeddingRequest request = new CohereEmbeddingRequest( List.of("I like to eat apples", "I like to eat oranges"), diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc index f45ea168e2..41327c9efa 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-annotations-examples.adoc @@ -489,14 +489,14 @@ public class StatelessTools { @McpToolParam(description = "JSON string", required = true) String json) { try { - ObjectMapper mapper = new ObjectMapper(); + JsonMapper mapper = new JsonMapper(); mapper.readTree(json); return CallToolResult.builder() .addTextContent("Valid JSON") .structuredContent(Map.of("valid", true)) .build(); - } catch (Exception e) { + } catch (JacksonException e) { return CallToolResult.builder() .addTextContent("Invalid JSON: " + e.getMessage()) .structuredContent(Map.of("valid", false, "error", e.getMessage())) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc index 289de9ebd4..008fdb0aa7 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc @@ -454,13 +454,13 @@ Configure MCP clients programmatically instead of using Spring Boot properties. ---- @Bean McpSyncClient client( - ObjectMapper objectMapper, + JsonMapper jsonMapper, McpSyncHttpClientRequestCustomizer requestCustomizer, McpClientCommonProperties commonProps ) { var transport = HttpClientStreamableHttpTransport.builder(mcpServerUrl) .clientBuilder(HttpClient.newBuilder()) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .httpRequestCustomizer(requestCustomizer) .build(); @@ -481,12 +481,12 @@ For WebClient-based clients: @Bean McpSyncClient client( WebClient.Builder mcpWebClientBuilder, - ObjectMapper objectMapper, + JsonMapper jsonMapper, McpClientCommonProperties commonProperties ) { var builder = mcpWebClientBuilder.baseUrl(mcpServerUrl); var transport = WebClientStreamableHttpTransport.builder(builder) - .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(jsonMapper)) .build(); var clientInfo = new McpSchema.Implementation("clientName", commonProperties.getVersion()); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stateless-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stateless-server-boot-starter-docs.adoc index 5a9f5f2686..36787f4e34 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stateless-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stateless-server-boot-starter-docs.adoc @@ -165,7 +165,7 @@ public List myResources(.. var resourceSpecification = new McpStatelessServerFeatures.SyncResourceSpecification(systemInfoResource, (context, request) -> { try { var systemInfo = Map.of(...); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult( List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stdio-sse-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stdio-sse-server-boot-starter-docs.adoc index 9ab347f576..78f1f8f35c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stdio-sse-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-stdio-sse-server-boot-starter-docs.adoc @@ -188,7 +188,7 @@ public List myResources(...) { var resourceSpecification = new McpServerFeatures.SyncResourceSpecification(systemInfoResource, (exchange, request) -> { try { var systemInfo = Map.of(...); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult( List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc index 876de90440..8eab0953d7 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc @@ -173,7 +173,7 @@ public List myResources(...) { var resourceSpecification = new McpServerFeatures.SyncResourceSpecification(systemInfoResource, (exchange, request) -> { try { var systemInfo = Map.of(...); - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + String jsonContent = new JsonMapper().writeValueAsString(systemInfo); return new McpSchema.ReadResourceResult( List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc index 75eb0deafe..3588fa745e 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc @@ -86,7 +86,7 @@ image::structured-output-hierarchy4.jpg[Structured Output Class Hierarchy, width * `AbstractConversionServiceOutputConverter` - Offers a pre-configured link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/support/GenericConversionService.html[GenericConversionService] for transforming LLM output into the desired format. No default `FormatProvider` implementation is provided. * `AbstractMessageOutputConverter` - Supplies a pre-configured https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/support/converter/MessageConverter.html[MessageConverter] for converting LLM output into the desired format. No default `FormatProvider` implementation is provided. -* `BeanOutputConverter` - Configured with a designated Java class (e.g., Bean) or a link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html[ParameterizedTypeReference], this converter employs a `FormatProvider` implementation that directs the AI Model to produce a JSON response compliant with a `DRAFT_2020_12`, `JSON Schema` derived from the specified Java class. Subsequently, it utilizes an `ObjectMapper` to deserialize the JSON output into a Java object instance of the target class. +* `BeanOutputConverter` - Configured with a designated Java class (e.g., Bean) or a link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html[ParameterizedTypeReference], this converter employs a `FormatProvider` implementation that directs the AI Model to produce a JSON response compliant with a `DRAFT_2020_12`, `JSON Schema` derived from the specified Java class. Subsequently, it utilizes an `JsonMapper` to deserialize the JSON output into a Java object instance of the target class. * `MapOutputConverter` - Extends the functionality of `AbstractMessageOutputConverter` with a `FormatProvider` implementation that guides the AI Model to generate an RFC8259 compliant JSON response. Additionally, it incorporates a converter implementation that utilizes the provided `MessageConverter` to translate the JSON payload into a `java.util.Map` instance. * `ListOutputConverter` - Extends the `AbstractConversionServiceOutputConverter` and includes a `FormatProvider` implementation tailored for comma-delimited list output. The converter implementation employs the provided `ConversionService` to transform the model text output into a `java.util.List`. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc index 79cbccef75..ba03c7b5a0 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/upgrade-notes.adoc @@ -478,13 +478,13 @@ Key changes include: [source,java] ---- // Before -ServerMcpTransport transport = new WebFluxSseServerTransport(objectMapper, "/mcp/message"); +ServerMcpTransport transport = new WebFluxSseServerTransport(jsonMapper, "/mcp/message"); var server = McpServer.sync(transport) .serverInfo("my-server", "1.0.0") .build(); // After -McpServerTransportProvider transportProvider = new WebFluxSseServerTransportProvider(objectMapper, "/mcp/message"); +McpServerTransportProvider transportProvider = new WebFluxSseServerTransportProvider(jsonMapper, "/mcp/message"); var server = McpServer.sync(transportProvider) .serverInfo("my-server", "1.0.0") .build(); diff --git a/spring-ai-model/pom.xml b/spring-ai-model/pom.xml index 851441f831..88f8305e7b 100644 --- a/spring-ai-model/pom.xml +++ b/spring-ai-model/pom.xml @@ -94,16 +94,10 @@ - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - com.github.victools jsonschema-module-swagger-2 @@ -142,7 +136,7 @@ - com.fasterxml.jackson.module + tools.jackson.module jackson-module-kotlin test 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 078ed066f8..328059e65c 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 @@ -20,24 +20,22 @@ import java.util.Map; import java.util.Objects; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.util.DefaultIndenter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; -import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; +import com.github.victools.jsonschema.module.jackson.JacksonSchemaModule; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.core.util.DefaultIndenter; +import tools.jackson.core.util.DefaultPrettyPrinter; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.model.KotlinModule; import org.springframework.ai.util.JacksonUtils; @@ -72,8 +70,8 @@ public class BeanOutputConverter implements StructuredOutputConverter { */ private final Type type; - /** The object mapper used for deserialization and other JSON operations. */ - private final ObjectMapper objectMapper; + /** The JSON mapper used for deserialization and other JSON operations. */ + private final JsonMapper jsonMapper; /** Holds the generated JSON schema for the target type. */ private String jsonSchema; @@ -90,25 +88,25 @@ public BeanOutputConverter(Class clazz) { } /** - * Constructor to initialize with the target type's class, a custom object mapper, and - * a line endings normalizer to ensure consistent line endings on any platform. + * Constructor to initialize with the target type's class, a custom JSON mapper, and a + * line endings normalizer to ensure consistent line endings on any platform. * @param clazz The target type's class. - * @param objectMapper Custom object mapper for JSON operations. endings. + * @param jsonMapper Custom JSON mapper for JSON operations. endings. */ - public BeanOutputConverter(Class clazz, @Nullable ObjectMapper objectMapper) { - this(clazz, objectMapper, null); + public BeanOutputConverter(Class clazz, @Nullable JsonMapper jsonMapper) { + this(clazz, jsonMapper, null); } /** - * Constructor to initialize with the target type's class, a custom object mapper, and - * a custom text cleaner. + * Constructor to initialize with the target type's class, a custom JSON mapper, and a + * custom text cleaner. * @param clazz The target type's class. - * @param objectMapper Custom object mapper for JSON operations. + * @param jsonMapper Custom JSON mapper for JSON operations. * @param textCleaner Custom text cleaner for preprocessing responses. */ - public BeanOutputConverter(Class clazz, @Nullable ObjectMapper objectMapper, + public BeanOutputConverter(Class clazz, @Nullable JsonMapper jsonMapper, @Nullable ResponseTextCleaner textCleaner) { - this(ParameterizedTypeReference.forType(clazz), objectMapper, textCleaner); + this(ParameterizedTypeReference.forType(clazz), jsonMapper, textCleaner); } /** @@ -120,41 +118,40 @@ public BeanOutputConverter(ParameterizedTypeReference typeRef) { } /** - * Constructor to initialize with the target class type reference, a custom object + * Constructor to initialize with the target class type reference, a custom JSON * mapper, and a line endings normalizer to ensure consistent line endings on any * platform. * @param typeRef The target class type reference. - * @param objectMapper Custom object mapper for JSON operations. endings. + * @param jsonMapper Custom JSON mapper for JSON operations. endings. */ - public BeanOutputConverter(ParameterizedTypeReference typeRef, @Nullable ObjectMapper objectMapper) { - this(typeRef, objectMapper, null); + public BeanOutputConverter(ParameterizedTypeReference typeRef, @Nullable JsonMapper jsonMapper) { + this(typeRef, jsonMapper, null); } /** - * Constructor to initialize with the target class type reference, a custom object + * Constructor to initialize with the target class type reference, a custom JSON * mapper, and a custom text cleaner. * @param typeRef The target class type reference. - * @param objectMapper Custom object mapper for JSON operations. + * @param jsonMapper Custom JSON mapper for JSON operations. * @param textCleaner Custom text cleaner for preprocessing responses. */ - public BeanOutputConverter(ParameterizedTypeReference typeRef, @Nullable ObjectMapper objectMapper, + public BeanOutputConverter(ParameterizedTypeReference typeRef, @Nullable JsonMapper jsonMapper, @Nullable ResponseTextCleaner textCleaner) { - this(typeRef.getType(), objectMapper, textCleaner); + this(typeRef.getType(), jsonMapper, textCleaner); } /** - * Constructor to initialize with the target class type reference, a custom object + * Constructor to initialize with the target class type reference, a custom JSON * mapper, and a line endings normalizer to ensure consistent line endings on any * platform. * @param type The target class type. - * @param objectMapper Custom object mapper for JSON operations. endings. + * @param jsonMapper Custom JSON mapper for JSON operations. endings. * @param textCleaner Custom text cleaner for preprocessing responses. */ - private BeanOutputConverter(Type type, @Nullable ObjectMapper objectMapper, - @Nullable ResponseTextCleaner textCleaner) { + private BeanOutputConverter(Type type, @Nullable JsonMapper jsonMapper, @Nullable ResponseTextCleaner textCleaner) { Objects.requireNonNull(type, "Type cannot be null;"); this.type = type; - this.objectMapper = objectMapper != null ? objectMapper : getObjectMapper(); + this.jsonMapper = jsonMapper != null ? jsonMapper : getJsonMapper(); this.textCleaner = textCleaner != null ? textCleaner : createDefaultTextCleaner(); generateSchema(); } @@ -189,7 +186,7 @@ private static ResponseTextCleaner createDefaultTextCleaner() { * Generates the JSON schema for the target type. */ private void generateSchema() { - JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, + JacksonSchemaModule jacksonModule = new JacksonSchemaModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder( com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12, @@ -207,12 +204,13 @@ private void generateSchema() { SchemaGenerator generator = new SchemaGenerator(config); JsonNode jsonNode = generator.generateSchema(this.type); postProcessSchema(jsonNode); - ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter() - .withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator()))); + ObjectWriter objectWriter = this.jsonMapper.writer() + .with(new DefaultPrettyPrinter() + .withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator()))); try { this.jsonSchema = objectWriter.writeValueAsString(jsonNode); } - catch (JsonProcessingException e) { + catch (JacksonException e) { logger.error("Could not pretty print json schema for jsonNode: {}", jsonNode); throw new RuntimeException("Could not pretty print json schema for " + this.type, e); } @@ -238,24 +236,21 @@ public T convert(String text) { // Clean the text using the configured text cleaner text = this.textCleaner.clean(text); - return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type)); + return (T) this.jsonMapper.readValue(text, this.jsonMapper.constructType(this.type)); } - catch (JsonProcessingException e) { + catch (JacksonException e) { logger.error(SENSITIVE_DATA_MARKER, "Could not parse the given text to the desired target type: \"{}\" into {}", text, this.type); - throw new RuntimeException(e); + throw e; } } /** - * Configures and returns an object mapper for JSON operations. - * @return Configured object mapper. + * Configures and returns a JSON mapper for JSON operations. + * @return Configured JSON mapper. */ - protected ObjectMapper getObjectMapper() { - return JsonMapper.builder() - .addModules(JacksonUtils.instantiateAvailableModules()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .build(); + protected JsonMapper getJsonMapper() { + return JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } /** @@ -286,9 +281,9 @@ public String getJsonSchema() { public Map getJsonSchemaMap() { try { - return this.objectMapper.readValue(this.jsonSchema, Map.class); + return this.jsonMapper.readValue(this.jsonSchema, Map.class); } - catch (JsonProcessingException ex) { + catch (JacksonException ex) { logger.error("Could not parse the JSON Schema to a Map object", ex); throw new IllegalStateException(ex); } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/converter/MapOutputConverter.java b/spring-ai-model/src/main/java/org/springframework/ai/converter/MapOutputConverter.java index 8d1a64c7cf..d3e64fda1c 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/converter/MapOutputConverter.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/converter/MapOutputConverter.java @@ -20,13 +20,16 @@ import java.util.HashMap; import java.util.Map; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; + import org.springframework.messaging.Message; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.support.MessageBuilder; /** * {@link StructuredOutputConverter} implementation that uses a pre-configured - * {@link MappingJackson2MessageConverter} to convert the LLM output into a + * {@link JacksonJsonMessageConverter} to convert the LLM output into a * java.util.Map<String, Object> instance. * * @author Mark Pollack @@ -35,7 +38,8 @@ public class MapOutputConverter extends AbstractMessageOutputConverter> { public MapOutputConverter() { - super(new MappingJackson2MessageConverter()); + super(new JacksonJsonMessageConverter( + JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS))); } @Override diff --git a/spring-ai-model/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java b/spring-ai-model/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java index 505cc4279b..8c06ef8dae 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java @@ -29,27 +29,22 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.cfg.CoercionAction; -import com.fasterxml.jackson.databind.cfg.CoercionInputShape; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.OptionPreset; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; -import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; +import com.github.victools.jsonschema.module.jackson.JacksonSchemaModule; import com.github.victools.jsonschema.module.swagger2.Swagger2Module; import org.jspecify.annotations.Nullable; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.util.JacksonUtils; import org.springframework.beans.BeanWrapper; @@ -69,18 +64,17 @@ */ public abstract class ModelOptionsUtils { - public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .addModules(JacksonUtils.instantiateAvailableModules()) - .build() - .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); + public static final JsonMapper JSON_MAPPER; static { // Configure coercion for empty strings to null for Enum types // This fixes the issue where empty string finish_reason values cause // deserialization failures - OBJECT_MAPPER.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + JSON_MAPPER = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) + .addModules(JacksonUtils.instantiateAvailableModules()) + .build(); } private static final List BEAN_MERGE_FIELD_EXCISIONS = List.of("class"); @@ -95,28 +89,23 @@ public abstract class ModelOptionsUtils { /** * Converts the given JSON string to a Map of String and Object using the default - * ObjectMapper. + * JsonMapper. * @param json the JSON string to convert to a Map. * @return the converted Map. */ public static Map jsonToMap(String json) { - return jsonToMap(json, OBJECT_MAPPER); + return jsonToMap(json, JSON_MAPPER); } /** * Converts the given JSON string to a Map of String and Object using a custom - * ObjectMapper. + * JsonMapper. * @param json the JSON string to convert to a Map. - * @param objectMapper the ObjectMapper to use for deserialization. + * @param jsonMapper the JsonMapper to use for deserialization. * @return the converted Map. */ - public static Map jsonToMap(String json, ObjectMapper objectMapper) { - try { - return objectMapper.readValue(json, MAP_TYPE_REF); - } - catch (Exception e) { - throw new RuntimeException(e); - } + public static Map jsonToMap(String json, JsonMapper jsonMapper) { + return jsonMapper.readValue(json, MAP_TYPE_REF); } /** @@ -127,12 +116,7 @@ public static Map jsonToMap(String json, ObjectMapper objectMapp * @return Object instance of the given type. */ public static T jsonToObject(String json, Class type) { - try { - return OBJECT_MAPPER.readValue(json, type); - } - catch (Exception e) { - throw new RuntimeException("Failed to json: " + json, e); - } + return JSON_MAPPER.readValue(json, type); } /** @@ -141,12 +125,7 @@ public static T jsonToObject(String json, Class type) { * @return the JSON string. */ public static String toJsonString(Object object) { - try { - return OBJECT_MAPPER.writeValueAsString(object); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return JSON_MAPPER.writeValueAsString(object); } /** @@ -155,12 +134,7 @@ public static String toJsonString(Object object) { * @return the JSON string. */ public static String toJsonStringPrettyPrinter(Object object) { - try { - return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object); } /** @@ -233,19 +207,14 @@ public static Map objectToMap(@Nullable Object source) { if (source == null) { return new HashMap<>(); } - try { - String json = OBJECT_MAPPER.writeValueAsString(source); - return OBJECT_MAPPER.readValue(json, new TypeReference>() { - - }) - .entrySet() - .stream() - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + String json = JSON_MAPPER.writeValueAsString(source); + return JSON_MAPPER.readValue(json, new TypeReference>() { + + }) + .entrySet() + .stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } /** @@ -256,13 +225,8 @@ public static Map objectToMap(@Nullable Object source) { * @return the converted class. */ public static T mapToClass(Map source, Class clazz) { - try { - String json = OBJECT_MAPPER.writeValueAsString(source); - return OBJECT_MAPPER.readValue(json, clazz); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + String json = JSON_MAPPER.writeValueAsString(source); + return JSON_MAPPER.readValue(json, clazz); } /** @@ -395,7 +359,7 @@ public static ObjectNode getJsonSchema(Type inputType) { if (SCHEMA_GENERATOR_CACHE.get() == null) { - JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED); + JacksonSchemaModule jacksonModule = new JacksonSchemaModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED); Swagger2Module swaggerModule = new Swagger2Module(); SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, @@ -429,13 +393,13 @@ public static void toUpperCaseTypeValues(ObjectNode node) { return; } if (node.isObject()) { - node.fields().forEachRemaining(entry -> { + node.properties().forEach(entry -> { JsonNode value = entry.getValue(); if (value.isObject()) { toUpperCaseTypeValues((ObjectNode) value); } else if (value.isArray()) { - ((ArrayNode) value).elements().forEachRemaining(element -> { + value.forEach(element -> { if (element.isObject() || element.isArray()) { toUpperCaseTypeValues((ObjectNode) element); } @@ -448,7 +412,7 @@ else if (value.isTextual() && entry.getKey().equals("type")) { }); } else if (node.isArray()) { - node.elements().forEachRemaining(element -> { + node.forEach(element -> { if (element.isObject() || element.isArray()) { toUpperCaseTypeValues((ObjectNode) element); } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallback.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallback.java index e69474f67a..c59496a243 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallback.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallback.java @@ -20,8 +20,8 @@ import java.util.Map; import java.util.function.Consumer; -import com.fasterxml.jackson.core.type.TypeReference; import org.jspecify.annotations.Nullable; +import tools.jackson.core.type.TypeReference; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.ToolCallback; diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenter.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenter.java index 388f9cca64..3b0f1960f1 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenter.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenter.java @@ -20,8 +20,8 @@ import java.util.Arrays; import java.util.List; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.tool.annotation.ToolParam; @@ -85,7 +85,7 @@ public static String augmentToolInputSchema(String jsonSchemaString, List extractToolArguments(String toolInput) { } catch (Exception ex) { logger.warn("Conversion from JSON failed", ex); - Throwable cause = (ex.getCause() instanceof JsonProcessingException) ? ex.getCause() : ex; + Throwable cause = (ex.getCause() instanceof JacksonException) ? ex.getCause() : ex; throw new ToolExecutionException(this.getToolDefinition(), cause); } } @@ -164,7 +164,7 @@ private Object[] buildMethodArguments(Map toolInputArguments, @N } catch (Exception ex) { logger.warn("Conversion from JSON failed", ex); - Throwable cause = (ex.getCause() instanceof JsonProcessingException) ? ex.getCause() : ex; + Throwable cause = (ex.getCause() instanceof JacksonException) ? ex.getCause() : ex; throw new ToolExecutionException(this.getToolDefinition(), cause); } } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/util/json/JsonParser.java b/spring-ai-model/src/main/java/org/springframework/ai/util/json/JsonParser.java index 25158ed531..de5beb6af8 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/util/json/JsonParser.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/util/json/JsonParser.java @@ -19,13 +19,12 @@ import java.lang.reflect.Type; import java.math.BigDecimal; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.util.JacksonUtils; import org.springframework.util.Assert; @@ -36,21 +35,25 @@ */ public final class JsonParser { - private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .addModules(JacksonUtils.instantiateAvailableModules()) - .build(); + private static final JsonMapper jsonMapper; + + static { + jsonMapper = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .addModules(JacksonUtils.instantiateAvailableModules()) + .build(); + } private JsonParser() { } /** - * Returns a Jackson {@link ObjectMapper} instance tailored for JSON-parsing - * operations for tool calling and structured output. + * Returns a Jackson {@link JsonMapper} instance tailored for JSON-parsing operations + * for tool calling and structured output. */ - public static ObjectMapper getObjectMapper() { - return OBJECT_MAPPER; + public static JsonMapper getJsonMapper() { + return jsonMapper; } /** @@ -61,9 +64,9 @@ public static T fromJson(String json, Class type) { Assert.notNull(type, "type cannot be null"); try { - return OBJECT_MAPPER.readValue(json, type); + return jsonMapper.readValue(json, type); } - catch (JsonProcessingException ex) { + catch (JacksonException ex) { throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getName()), ex); } } @@ -76,9 +79,9 @@ public static T fromJson(String json, Type type) { Assert.notNull(type, "type cannot be null"); try { - return OBJECT_MAPPER.readValue(json, OBJECT_MAPPER.constructType(type)); + return jsonMapper.readValue(json, jsonMapper.constructType(type)); } - catch (JsonProcessingException ex) { + catch (JacksonException ex) { throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getTypeName()), ex); } } @@ -91,9 +94,9 @@ public static T fromJson(String json, TypeReference type) { Assert.notNull(type, "type cannot be null"); try { - return OBJECT_MAPPER.readValue(json, type); + return jsonMapper.readValue(json, type); } - catch (JsonProcessingException ex) { + catch (JacksonException ex) { throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getType().getTypeName()), ex); } @@ -104,10 +107,10 @@ public static T fromJson(String json, TypeReference type) { */ private static boolean isValidJson(String input) { try { - OBJECT_MAPPER.readTree(input); + jsonMapper.readTree(input); return true; } - catch (JsonProcessingException e) { + catch (JacksonException e) { return false; } } @@ -120,9 +123,9 @@ public static String toJson(@Nullable Object object) { return str; } try { - return OBJECT_MAPPER.writeValueAsString(object); + return jsonMapper.writeValueAsString(object); } - catch (JsonProcessingException ex) { + catch (JacksonException ex) { throw new IllegalStateException("Conversion from Object to JSON failed", ex); } } @@ -173,7 +176,7 @@ else if (javaType.isEnum()) { try { result = JsonParser.fromJson(jsonString, javaType); } - catch (Exception e) { + catch (JacksonException e) { // ignore } } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java index 1f80f5e525..aeeae5cc7a 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java @@ -25,8 +25,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.victools.jsonschema.generator.Module; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.OptionPreset; @@ -34,11 +32,13 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; -import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; +import com.github.victools.jsonschema.module.jackson.JacksonSchemaModule; import com.github.victools.jsonschema.module.swagger2.Swagger2Module; import io.swagger.v3.oas.annotations.media.Schema; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.annotation.ToolParam; @@ -90,7 +90,7 @@ public final class JsonSchemaGenerator { * Initialize JSON Schema generators. */ static { - Module jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED); + Module jacksonModule = new JacksonSchemaModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED); Module openApiModule = new Swagger2Module(); Module springAiSchemaModule = PROPERTY_REQUIRED_BY_DEFAULT ? new SpringAiSchemaModule() : new SpringAiSchemaModule(SpringAiSchemaModule.Option.PROPERTY_REQUIRED_FALSE_BY_DEFAULT); @@ -119,7 +119,7 @@ private JsonSchemaGenerator() { * Generate a JSON Schema for a method's input parameters. */ public static String generateForMethodInput(Method method, SchemaOption... schemaOptions) { - ObjectNode schema = JsonParser.getObjectMapper().createObjectNode(); + ObjectNode schema = JsonParser.getJsonMapper().createObjectNode(); schema.put("$schema", SchemaVersion.DRAFT_2020_12.getIdentifier()); schema.put("type", "object"); @@ -259,13 +259,13 @@ private static boolean isMethodParameterRequired(Method method, int index) { // Based on the method in ModelOptionsUtils. public static void convertTypeValuesToUpperCase(ObjectNode node) { if (node.isObject()) { - node.fields().forEachRemaining(entry -> { + node.properties().forEach(entry -> { JsonNode value = entry.getValue(); if (value.isObject()) { convertTypeValuesToUpperCase((ObjectNode) value); } else if (value.isArray()) { - value.elements().forEachRemaining(element -> { + value.forEach(element -> { if (element.isObject() || element.isArray()) { convertTypeValuesToUpperCase((ObjectNode) element); } @@ -278,7 +278,7 @@ else if (value.isTextual() && entry.getKey().equals("type")) { }); } else if (node.isArray()) { - node.elements().forEachRemaining(element -> { + node.forEach(element -> { if (element.isObject() || element.isArray()) { convertTypeValuesToUpperCase((ObjectNode) element); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/metadata/DefaultUsageTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/metadata/DefaultUsageTests.java index cada62d555..025ec085e3 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/chat/metadata/DefaultUsageTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/metadata/DefaultUsageTests.java @@ -19,26 +19,24 @@ import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; public class DefaultUsageTests { - private final ObjectMapper objectMapper = new ObjectMapper(); - @Test void testSerializationWithAllFields() throws Exception { DefaultUsage usage = new DefaultUsage(Integer.valueOf(100), Integer.valueOf(50), Integer.valueOf(150)); - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo("{\"promptTokens\":100,\"completionTokens\":50,\"totalTokens\":150}"); } @Test void testDeserializationWithAllFields() throws Exception { String json = "{\"promptTokens\":100,\"completionTokens\":50,\"totalTokens\":150}"; - DefaultUsage usage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage usage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(usage.getPromptTokens()).isEqualTo(100); assertThat(usage.getCompletionTokens()).isEqualTo(50); assertThat(usage.getTotalTokens()).isEqualTo(150); @@ -47,14 +45,14 @@ void testDeserializationWithAllFields() throws Exception { @Test void testSerializationWithNullFields() throws Exception { DefaultUsage usage = new DefaultUsage((Integer) null, (Integer) null, (Integer) null); - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo("{\"promptTokens\":0,\"completionTokens\":0,\"totalTokens\":0}"); } @Test void testDeserializationWithMissingFields() throws Exception { String json = "{\"promptTokens\":100}"; - DefaultUsage usage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage usage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(usage.getPromptTokens()).isEqualTo(100); assertThat(usage.getCompletionTokens()).isEqualTo(0); assertThat(usage.getTotalTokens()).isEqualTo(100); @@ -63,7 +61,7 @@ void testDeserializationWithMissingFields() throws Exception { @Test void testDeserializationWithNullFields() throws Exception { String json = "{\"promptTokens\":null,\"completionTokens\":null,\"totalTokens\":null}"; - DefaultUsage usage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage usage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(usage.getPromptTokens()).isEqualTo(0); assertThat(usage.getCompletionTokens()).isEqualTo(0); assertThat(usage.getTotalTokens()).isEqualTo(0); @@ -72,8 +70,8 @@ void testDeserializationWithNullFields() throws Exception { @Test void testRoundTripSerialization() throws Exception { DefaultUsage original = new DefaultUsage(Integer.valueOf(100), Integer.valueOf(50), Integer.valueOf(150)); - String json = this.objectMapper.writeValueAsString(original); - DefaultUsage deserialized = this.objectMapper.readValue(json, DefaultUsage.class); + String json = JsonMapper.shared().writeValueAsString(original); + DefaultUsage deserialized = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(deserialized.getPromptTokens()).isEqualTo(original.getPromptTokens()); assertThat(deserialized.getCompletionTokens()).isEqualTo(original.getCompletionTokens()); assertThat(deserialized.getTotalTokens()).isEqualTo(original.getTotalTokens()); @@ -89,11 +87,11 @@ void testTwoArgumentConstructorAndSerialization() throws Exception { assertThat(usage.getTotalTokens()).isEqualTo(150); // 100 + 50 = 150 // Test serialization - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo("{\"promptTokens\":100,\"completionTokens\":50,\"totalTokens\":150}"); // Test deserialization - DefaultUsage deserializedUsage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage deserializedUsage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(deserializedUsage.getPromptTokens()).isEqualTo(100); assertThat(deserializedUsage.getCompletionTokens()).isEqualTo(50); assertThat(deserializedUsage.getTotalTokens()).isEqualTo(150); @@ -109,11 +107,11 @@ void testTwoArgumentConstructorWithNullValues() throws Exception { assertThat(usage.getTotalTokens()).isEqualTo(0); // Test serialization - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo("{\"promptTokens\":0,\"completionTokens\":0,\"totalTokens\":0}"); // Test deserialization - DefaultUsage deserializedUsage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage deserializedUsage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(deserializedUsage.getPromptTokens()).isEqualTo(0); assertThat(deserializedUsage.getCompletionTokens()).isEqualTo(0); assertThat(deserializedUsage.getTotalTokens()).isEqualTo(0); @@ -122,7 +120,7 @@ void testTwoArgumentConstructorWithNullValues() throws Exception { @Test void testDeserializationWithDifferentPropertyOrder() throws Exception { String json = "{\"totalTokens\":150,\"completionTokens\":50,\"promptTokens\":100}"; - DefaultUsage usage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage usage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(usage.getPromptTokens()).isEqualTo(100); assertThat(usage.getCompletionTokens()).isEqualTo(50); assertThat(usage.getTotalTokens()).isEqualTo(150); @@ -135,7 +133,7 @@ void testSerializationWithCustomNativeUsage() throws Exception { customNativeUsage.put("custom_number", 42); DefaultUsage usage = new DefaultUsage(100, 50, 150, customNativeUsage); - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo( "{\"promptTokens\":100,\"completionTokens\":50,\"totalTokens\":150,\"nativeUsage\":{\"custom_field\":\"custom_value\",\"custom_number\":42}}"); } @@ -143,7 +141,7 @@ void testSerializationWithCustomNativeUsage() throws Exception { @Test void testDeserializationWithCustomNativeUsage() throws Exception { String json = "{\"promptTokens\":100,\"completionTokens\":50,\"totalTokens\":150,\"nativeUsage\":{\"custom_field\":\"custom_value\",\"custom_number\":42}}"; - DefaultUsage usage = this.objectMapper.readValue(json, DefaultUsage.class); + DefaultUsage usage = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(usage.getPromptTokens()).isEqualTo(100); assertThat(usage.getCompletionTokens()).isEqualTo(50); assertThat(usage.getTotalTokens()).isEqualTo(150); @@ -165,8 +163,8 @@ void testArbitraryNativeUsageMap() throws Exception { DefaultUsage usage = new DefaultUsage(100, 50, 150, arbitraryMap); - String json = this.objectMapper.writeValueAsString(usage); - DefaultUsage deserialized = this.objectMapper.readValue(json, DefaultUsage.class); + String json = JsonMapper.shared().writeValueAsString(usage); + DefaultUsage deserialized = JsonMapper.shared().readValue(json, DefaultUsage.class); assertThat(deserialized.getPromptTokens()).isEqualTo(usage.getPromptTokens()); assertThat(deserialized.getCompletionTokens()).isEqualTo(usage.getCompletionTokens()); @@ -241,7 +239,7 @@ void testNegativeTokenValues() throws Exception { assertThat(usage.getCompletionTokens()).isEqualTo(-2); assertThat(usage.getTotalTokens()).isEqualTo(-3); - String json = this.objectMapper.writeValueAsString(usage); + String json = JsonMapper.shared().writeValueAsString(usage); assertThat(json).isEqualTo("{\"promptTokens\":-1,\"completionTokens\":-2,\"totalTokens\":-3}"); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java b/spring-ai-model/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java index b9b8f473aa..adbb453a90 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java @@ -26,10 +26,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -37,6 +33,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.util.TextBlockAssertion; import org.springframework.core.ParameterizedTypeReference; @@ -58,7 +57,7 @@ class BeanOutputConverterTest { private ListAppender logAppender; @Mock - private ObjectMapper objectMapperMock; + private JsonMapper jsonMapperMock; @BeforeEach void beforeEach() { @@ -75,8 +74,8 @@ void shouldHavePreConfiguredDefaultObjectMapper() { var converter = new BeanOutputConverter<>(new ParameterizedTypeReference() { }); - var objectMapper = converter.getObjectMapper(); - assertThat(objectMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + var jsonMapper = converter.getJsonMapper(); + assertThat(jsonMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); } static class TestClass { @@ -160,7 +159,7 @@ void convertClassType() { @Test void failToConvertInvalidJson() { var converter = new BeanOutputConverter<>(TestClass.class); - assertThatThrownBy(() -> converter.convert("{invalid json")).hasCauseInstanceOf(JsonParseException.class); + assertThatThrownBy(() -> converter.convert("{invalid json")).hasMessageStartingWith("Unexpected character"); assertThat(BeanOutputConverterTest.this.logAppender.list).hasSize(1); final var loggingEvent = BeanOutputConverterTest.this.logAppender.list.get(0); assertThat(loggingEvent.getFormattedMessage()) @@ -208,11 +207,9 @@ void verifySchemaPropertyOrder() throws Exception { var converter = new BeanOutputConverter<>(TestClassWithJsonPropertyOrder.class); String jsonSchema = converter.getJsonSchema(); - ObjectMapper mapper = new ObjectMapper(); - JsonNode schemaNode = mapper.readTree(jsonSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(jsonSchema); - List actualOrder = new ArrayList<>(); - schemaNode.get("properties").fieldNames().forEachRemaining(actualOrder::add); + List actualOrder = new ArrayList<>(schemaNode.get("properties").propertyNames()); assertThat(actualOrder).containsExactly("string_property", "foo_property", "bar_property"); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/model/ModelOptionsUtilsTests.java b/spring-ai-model/src/test/java/org/springframework/ai/model/ModelOptionsUtilsTests.java index 732a5cba1a..f5cc56c00d 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/model/ModelOptionsUtilsTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/model/ModelOptionsUtilsTests.java @@ -19,12 +19,10 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; -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 com.fasterxml.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -135,12 +133,11 @@ public void jsonToMap_emptyStringAsNullObject() { assertThat(map.get("name")).isEqualTo(""); assertThat(map.get("age")).isEqualTo(30); - // Custom ObjectMapper: still "" for Map - ObjectMapper strictMapper = JsonMapper.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Custom JsonMapper: still "" for Map + JsonMapper strictMapper = JsonMapper.builder() .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .build() - .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false); + .disable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) + .build(); Map mapStrict = ModelOptionsUtils.jsonToMap(json, strictMapper); assertThat(mapStrict.get("name")).isEqualTo(""); } @@ -150,22 +147,21 @@ public void pojo_emptyStringAsNullObject() throws Exception { String json = "{\"name\":\"\", \"age\":30}"; // POJO with default OBJECT_MAPPER (feature enabled) - Person person = ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Person.class); + Person person = ModelOptionsUtils.JSON_MAPPER.readValue(json, Person.class); assertThat(person.name).isEqualTo(""); // String remains "" assertThat(person.age).isEqualTo(30); // Integer is fine String jsonWithEmptyAge = "{\"name\":\"John\", \"age\":\"\"}"; - Person person2 = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyAge, Person.class); + Person person2 = ModelOptionsUtils.JSON_MAPPER.readValue(jsonWithEmptyAge, Person.class); assertThat(person2.name).isEqualTo("John"); assertThat(person2.age).isNull(); // Integer: "" → null // TODO: Need to investigate why the below fails // // POJO with feature disabled: should fail for Integer field - // ObjectMapper strictMapper = JsonMapper.builder() - // .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // JsonMapper strictMapper = JsonMapper.builder() // .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - // .build() - // .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false); + // .disable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) + // .build(); // assertThatThrownBy(() -> strictMapper.readValue(jsonWithEmptyAge, // Person.class)).isInstanceOf(Exception.class); } @@ -180,42 +176,42 @@ record TestRecord(@JsonProperty("field1") String fieldA, @JsonProperty("field2") } @Test - public void enumCoercion_emptyStringAsNull() throws JsonProcessingException { + public void enumCoercion_emptyStringAsNull() { // Test direct enum deserialization with empty string - ColorEnum colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"\"", ColorEnum.class); + ColorEnum colorEnum = ModelOptionsUtils.JSON_MAPPER.readValue("\"\"", ColorEnum.class); assertThat(colorEnum).isNull(); // Test direct enum deserialization with valid value - colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"RED\"", ColorEnum.class); + colorEnum = ModelOptionsUtils.JSON_MAPPER.readValue("\"RED\"", ColorEnum.class); assertThat(colorEnum).isEqualTo(ColorEnum.RED); // Test direct enum deserialization with invalid value should throw exception final String jsonInvalid = "\"Invalid\""; - assertThatThrownBy(() -> ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonInvalid, ColorEnum.class)) - .isInstanceOf(JsonProcessingException.class); + assertThatThrownBy(() -> ModelOptionsUtils.JSON_MAPPER.readValue(jsonInvalid, ColorEnum.class)) + .isInstanceOf(RuntimeException.class); } @Test - public void enumCoercion_objectMapperConfiguration() throws JsonProcessingException { - // Test that ModelOptionsUtils.OBJECT_MAPPER has the correct coercion + public void enumCoercion_jsonMapperConfiguration() { + // Test that ModelOptionsUtils.JSON_MAPPER has the correct coercion // configuration // This validates that our static configuration block is working // Empty string should coerce to null for enums - ColorEnum colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"\"", ColorEnum.class); + ColorEnum colorEnum = ModelOptionsUtils.JSON_MAPPER.readValue("\"\"", ColorEnum.class); assertThat(colorEnum).isNull(); // Null should remain null - colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("null", ColorEnum.class); + colorEnum = ModelOptionsUtils.JSON_MAPPER.readValue("null", ColorEnum.class); assertThat(colorEnum).isNull(); // Valid enum values should deserialize correctly - colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"BLUE\"", ColorEnum.class); + colorEnum = ModelOptionsUtils.JSON_MAPPER.readValue("\"BLUE\"", ColorEnum.class); assertThat(colorEnum).isEqualTo(ColorEnum.BLUE); } @Test - public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingException { + public void enumCoercion_apiResponseWithFinishReason() { // Test case 1: Empty string finish_reason should deserialize to null String jsonWithEmptyFinishReason = """ { @@ -224,7 +220,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce } """; - TestApiResponse response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyFinishReason, + TestApiResponse response = ModelOptionsUtils.JSON_MAPPER.readValue(jsonWithEmptyFinishReason, TestApiResponse.class); assertThat(response.id()).isEqualTo("test-123"); assertThat(response.finishReason()).isNull(); @@ -238,7 +234,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce } """; - response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithValidFinishReason, TestApiResponse.class); + response = ModelOptionsUtils.JSON_MAPPER.readValue(jsonWithValidFinishReason, TestApiResponse.class); assertThat(response.id()).isEqualTo("test-456"); assertThat(response.finishReason()).isEqualTo(TestFinishReason.STOP); @@ -250,7 +246,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce } """; - response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithNullFinishReason, TestApiResponse.class); + response = ModelOptionsUtils.JSON_MAPPER.readValue(jsonWithNullFinishReason, TestApiResponse.class); assertThat(response.id()).isEqualTo("test-789"); assertThat(response.finishReason()).isNull(); @@ -263,8 +259,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce """; assertThatThrownBy( - () -> ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithInvalidFinishReason, TestApiResponse.class)) - .isInstanceOf(JsonProcessingException.class) + () -> ModelOptionsUtils.JSON_MAPPER.readValue(jsonWithInvalidFinishReason, TestApiResponse.class)) .hasMessageContaining("INVALID_VALUE"); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/AugmentedToolCallbackTest.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/AugmentedToolCallbackTest.java index 0a8e5964a7..37929ea444 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/AugmentedToolCallbackTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/AugmentedToolCallbackTest.java @@ -19,8 +19,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -53,8 +53,6 @@ */ class AugmentedToolCallbackTest { - private static final ObjectMapper objectMapper = new ObjectMapper(); - @Mock private ToolCallback mockDelegate; @@ -212,7 +210,7 @@ void shouldAugmentToolDefinitionSchema() throws Exception { ToolDefinition augmentedDefinition = callback.getToolDefinition(); String augmentedSchema = augmentedDefinition.inputSchema(); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check original field is preserved assertTrue(schemaNode.get("properties").has("originalField")); @@ -382,7 +380,7 @@ void shouldRemoveExtendedArgumentsWhenConfigured() throws Exception { // Then verify(mockDelegate).call(argThat(input -> { try { - JsonNode inputNode = objectMapper.readTree(input); + JsonNode inputNode = JsonMapper.shared().readTree(input); return inputNode.has("originalField") && !inputNode.has("name") && !inputNode.has("age"); } catch (Exception e) { @@ -433,7 +431,7 @@ void shouldPreserveExtendedArgumentsWhenNotConfiguredToRemove() throws Exception // Then verify(mockDelegate).call(argThat(input -> { try { - JsonNode inputNode = objectMapper.readTree(input); + JsonNode inputNode = JsonMapper.shared().readTree(input); return inputNode.has("originalField") && inputNode.has("name") && inputNode.has("age"); } catch (Exception e) { @@ -539,7 +537,7 @@ void shouldHandleCompleteWorkflowWithConsumerProcessing() { // Verify delegate was called with cleaned input (extended args removed) verify(mockDelegate).call(argThat(input -> { try { - JsonNode inputNode = objectMapper.readTree(input); + JsonNode inputNode = JsonMapper.shared().readTree(input); return inputNode.has("productId") && !inputNode.has("value"); } catch (Exception e) { diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenterTest.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenterTest.java index 28e936b520..337df87841 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenterTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/augment/ToolInputSchemaAugmenterTest.java @@ -18,8 +18,8 @@ import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -41,8 +41,6 @@ */ class ToolInputSchemaAugmenterTest { - private static final ObjectMapper objectMapper = new ObjectMapper(); - // Test record classes public record SimpleRecord(@ToolParam(description = "A simple string field", required = true) String name, @ToolParam(description = "A simple integer field", required = false) int age) { @@ -230,7 +228,7 @@ void shouldAugmentSchemaWithSingleProperty() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(this.baseSchema, "newField", String.class, "A new field", true); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that new property was added assertTrue(schemaNode.get("properties").has("newField")); @@ -262,7 +260,7 @@ void shouldAugmentSchemaWithMultipleProperties() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(this.baseSchema, argumentTypes); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that both new properties were added assertTrue(schemaNode.get("properties").has("field1")); @@ -302,7 +300,7 @@ void shouldHandleSchemaWithoutExistingProperties() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(minimalSchema, "newField", String.class, "A new field", true); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that properties object was created assertTrue(schemaNode.has("properties")); @@ -334,7 +332,7 @@ void shouldHandleSchemaWithoutExistingRequiredArray() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(schemaWithoutRequired, "newField", String.class, "A new field", true); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that required array was created assertTrue(schemaNode.has("required")); @@ -349,7 +347,7 @@ void shouldHandleEmptyDescription() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(this.baseSchema, "newField", String.class, "", false); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); JsonNode newFieldNode = schemaNode.get("properties").get("newField"); // Should not have description property when empty @@ -362,7 +360,7 @@ void shouldHandleNullDescription() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(this.baseSchema, "newField", String.class, null, false); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); JsonNode newFieldNode = schemaNode.get("properties").get("newField"); // Should not have description property when null @@ -385,7 +383,7 @@ void shouldAugmentSchemaUsingRecordClass() throws Exception { .toAugmentedArgumentTypes(SimpleRecord.class); String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(this.baseSchema, argumentTypes); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that record fields were added assertTrue(schemaNode.get("properties").has("name")); @@ -442,7 +440,7 @@ void shouldHandleCompleteWorkflow() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(originalSchema, argumentTypes); // Verify the result - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Original field should still be there assertTrue(schemaNode.get("properties").has("productId")); @@ -493,7 +491,7 @@ void shouldPreserveSchemaStructureAndMetadata() throws Exception { String augmentedSchema = ToolInputSchemaAugmenter.augmentToolInputSchema(complexSchema, "newField", String.class, "New field", false); - JsonNode schemaNode = objectMapper.readTree(augmentedSchema); + JsonNode schemaNode = JsonMapper.shared().readTree(augmentedSchema); // Check that metadata is preserved assertEquals("https://json-schema.org/draft/2020-12/schema", schemaNode.get("$schema").asText()); diff --git a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonParserTests.java b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonParserTests.java index 30f2ac9251..00af3745a5 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonParserTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonParserTests.java @@ -18,8 +18,8 @@ import java.lang.reflect.Type; -import com.fasterxml.jackson.core.type.TypeReference; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -32,9 +32,9 @@ class JsonParserTests { @Test - void shouldGetObjectMapper() { - var objectMapper = JsonParser.getObjectMapper(); - assertThat(objectMapper).isNotNull(); + void shouldGetJsonMapper() { + var jsonMapper = JsonParser.getJsonMapper(); + assertThat(jsonMapper).isNotNull(); } @Test diff --git a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java index 00fd8325f3..7e65f1aa5e 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java @@ -26,10 +26,9 @@ import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.annotation.ToolParam; @@ -246,7 +245,7 @@ void generateSchemaForMethodWithAdditionalPropertiesAllowed() throws Exception { String schema = JsonSchemaGenerator.generateForMethodInput(method, JsonSchemaGenerator.SchemaOption.ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT); - JsonNode jsonNode = JsonParser.getObjectMapper().readTree(schema); + JsonNode jsonNode = JsonParser.getJsonMapper().readTree(schema); assertThat(jsonNode.has("additionalProperties")).isFalse(); } @@ -427,11 +426,11 @@ void generateSchemaForSimpleType() { } @Test - void generateSchemaForTypeWithAdditionalPropertiesAllowed() throws JsonProcessingException { + void generateSchemaForTypeWithAdditionalPropertiesAllowed() { String schema = JsonSchemaGenerator.generateForType(Person.class, JsonSchemaGenerator.SchemaOption.ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT); - JsonNode jsonNode = JsonParser.getObjectMapper().readTree(schema); + JsonNode jsonNode = JsonParser.getJsonMapper().readTree(schema); assertThat(jsonNode.has("additionalProperties")).isFalse(); } @@ -670,7 +669,7 @@ void generateSchemaForEnum() { } @Test - void generateSchemaForTypeWithJSpecifyNullableField() throws JsonProcessingException { + void generateSchemaForTypeWithJSpecifyNullableField() { String schema = JsonSchemaGenerator.generateForType(JSpecifyNullablePerson.class); String expectedJsonSchema = """ { 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 index 57f0dcb1bd..0e15a93203 100644 --- 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 @@ -16,16 +16,16 @@ package org.springframework.ai.converter -import com.fasterxml.jackson.databind.ObjectMapper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import tools.jackson.databind.json.JsonMapper 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() + private val jsonMapper = JsonMapper() @Test fun `test Kotlin data class schema generation using getJsonSchema`() { @@ -33,7 +33,7 @@ class KotlinBeanOutputConverterTests { val schemaJson = converter.jsonSchema - val schemaNode = objectMapper.readTree(schemaJson) + val schemaNode = jsonMapper.readTree(schemaJson) val required = schemaNode["required"] assertThat(required).isNotNull @@ -41,14 +41,14 @@ class KotlinBeanOutputConverterTests { assertThat(required.toString()).contains("baz") val properties = schemaNode["properties"] - assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + assertThat(properties["bar"]["type"].asString()).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") + assertThat(bazTypeNode.asString()).isEqualTo("string") } } @@ -58,7 +58,7 @@ class KotlinBeanOutputConverterTests { val schemaJson = converter.jsonSchema - val schemaNode = objectMapper.readTree(schemaJson) + val schemaNode = jsonMapper.readTree(schemaJson) val required = schemaNode["required"] assertThat(required).isNotNull @@ -66,9 +66,9 @@ class KotlinBeanOutputConverterTests { assertThat(required.toString()).contains("baz") val properties = schemaNode["properties"] - assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + assertThat(properties["bar"]["type"].asString()).isEqualTo("string") val bazTypeNode = properties["baz"]["type"] - assertThat(bazTypeNode.asText()).isEqualTo("integer") + assertThat(bazTypeNode.asString()).isEqualTo("integer") } } diff --git a/spring-ai-model/src/test/kotlin/org/springframework/ai/model/ModelOptionsUtilsTests.kt b/spring-ai-model/src/test/kotlin/org/springframework/ai/model/ModelOptionsUtilsTests.kt index 64b81b6475..aa40a33245 100644 --- a/spring-ai-model/src/test/kotlin/org/springframework/ai/model/ModelOptionsUtilsTests.kt +++ b/spring-ai-model/src/test/kotlin/org/springframework/ai/model/ModelOptionsUtilsTests.kt @@ -16,9 +16,9 @@ package org.springframework.ai.model -import com.fasterxml.jackson.databind.ObjectMapper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import tools.jackson.databind.json.JsonMapper import java.lang.reflect.Type class KotlinModelOptionsUtilsTests { @@ -26,7 +26,7 @@ class KotlinModelOptionsUtilsTests { private class Foo(val bar: String, val baz: String?) private class FooWithDefault(val bar: String, val baz: Int = 10) - private val objectMapper = ObjectMapper() + private val jsonMapper = JsonMapper() @Test fun `test ModelOptionsUtils with Kotlin data class`() { @@ -47,7 +47,7 @@ class KotlinModelOptionsUtilsTests { val schemaJson = ModelOptionsUtils.getJsonSchema(inputType, false) - val schemaNode = objectMapper.readTree(schemaJson) + val schemaNode = jsonMapper.readTree(schemaJson) val required = schemaNode["required"] assertThat(required).isNotNull @@ -55,14 +55,14 @@ class KotlinModelOptionsUtilsTests { assertThat(required.toString()).doesNotContain("baz") val properties = schemaNode["properties"] - assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + assertThat(properties["bar"]["type"].asString()).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") + assertThat(bazTypeNode.asString()).isEqualTo("string") } } @@ -72,7 +72,7 @@ class KotlinModelOptionsUtilsTests { val schemaJson = ModelOptionsUtils.getJsonSchema(inputType, false) - val schemaNode = objectMapper.readTree(schemaJson) + val schemaNode = jsonMapper.readTree(schemaJson) val required = schemaNode["required"] assertThat(required).isNotNull @@ -80,9 +80,9 @@ class KotlinModelOptionsUtilsTests { assertThat(required.toString()).doesNotContain("baz") val properties = schemaNode["properties"] - assertThat(properties["bar"]["type"].asText()).isEqualTo("string") + assertThat(properties["bar"]["type"].asString()).isEqualTo("string") val bazTypeNode = properties["baz"]["type"] - assertThat(bazTypeNode.asText()).isEqualTo("integer") + assertThat(bazTypeNode.asString()).isEqualTo("integer") } } diff --git a/spring-ai-rag/pom.xml b/spring-ai-rag/pom.xml index 316156de26..8bb644d701 100644 --- a/spring-ai-rag/pom.xml +++ b/spring-ai-rag/pom.xml @@ -62,7 +62,7 @@ - com.fasterxml.jackson.module + tools.jackson.module jackson-module-kotlin test diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryIT.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryIT.java index 878aa32764..49ba891838 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryIT.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryIT.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.testcontainers.chromadb.ChromaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -89,8 +89,8 @@ public void addAndSearchWithFilters() { static class Config { @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); + public JsonMapper jsonMapper() { + return new JsonMapper(); } @Bean diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryIT.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryIT.java index 067fbfef31..9eb73ee9ca 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryIT.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithToken2ContainerConnectionDetailsFactoryIT.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.testcontainers.chromadb.ChromaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -93,8 +93,8 @@ public void addAndSearchWithFilters() { static class Config { @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); + public JsonMapper jsonMapper() { + return new JsonMapper(); } @Bean diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryIT.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryIT.java index a035c3db2b..ab3098dad9 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryIT.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryIT.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.testcontainers.chromadb.ChromaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -91,8 +91,8 @@ public void addAndSearchWithFilters() { static class Config { @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); + public JsonMapper jsonMapper() { + return new JsonMapper(); } @Bean diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java index 70c1b4deb5..0c2b63459a 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java @@ -33,14 +33,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -81,7 +80,7 @@ public class SimpleVectorStore extends AbstractObservationVectorStore { private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class); - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private final ExpressionParser expressionParser; @@ -91,7 +90,7 @@ public class SimpleVectorStore extends AbstractObservationVectorStore { protected SimpleVectorStore(SimpleVectorStoreBuilder builder) { super(builder); - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); this.expressionParser = new SpelExpressionParser(); this.filterExpressionConverter = new SimpleVectorStoreFilterExpressionConverter(); } @@ -215,12 +214,7 @@ public void load(File file) { TypeReference> typeRef = new TypeReference<>() { }; - try { - this.store = this.objectMapper.readValue(file, typeRef); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } + this.store = this.jsonMapper.readValue(file, typeRef); } /** @@ -232,7 +226,7 @@ public void load(Resource resource) { }; try { - this.store = this.objectMapper.readValue(resource.getInputStream(), typeRef); + this.store = this.jsonMapper.readValue(resource.getInputStream(), typeRef); } catch (IOException ex) { throw new RuntimeException(ex); @@ -240,12 +234,12 @@ public void load(Resource resource) { } private String getVectorDbAsJson() { - ObjectWriter objectWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); + ObjectWriter objectWriter = this.jsonMapper.writerWithDefaultPrettyPrinter(); try { return objectWriter.writeValueAsString(this.store); } - catch (JsonProcessingException e) { - throw new RuntimeException("Error serializing documentMap to JSON.", e); + catch (JacksonException ex) { + throw new RuntimeException("Error serializing documentMap to JSON.", ex); } } diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java index 566c2881ac..4ea32abd7a 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jspecify.annotations.Nullable; import org.springframework.ai.content.Content; import org.springframework.ai.document.Document; @@ -53,7 +54,6 @@ public final class SimpleVectorStoreContent implements Content { * @param text the content text, must not be null * @param embedding the embedding vector, must not be null */ - @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") String text, @JsonProperty("embedding") float[] embedding) { this(text, new HashMap<>(), embedding); @@ -90,14 +90,20 @@ public SimpleVectorStoreContent(String text, Map metadata, IdGen * @param embedding the embedding vector, must not be null * @throws IllegalArgumentException if any parameter is null or if id is empty */ - public SimpleVectorStoreContent(String id, String text, Map metadata, float[] embedding) { - Assert.hasText(id, "id must not be null or empty"); + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public SimpleVectorStoreContent(@JsonProperty("id") @Nullable String id, + @JsonProperty("text") @JsonAlias("content") String text, + @JsonProperty("metadata") Map metadata, @JsonProperty("embedding") float[] embedding) { + + if (id != null) { + Assert.hasText(id, "id must not be null or empty"); + } Assert.notNull(text, "content must not be null"); Assert.notNull(metadata, "metadata must not be null"); Assert.notNull(embedding, "embedding must not be null"); Assert.isTrue(embedding.length > 0, "embedding vector must not be empty"); - this.id = id; + this.id = (id != null ? id : new RandomIdGenerator().generateId(text, metadata)); this.text = text; this.metadata = Map.copyOf(metadata); this.embedding = Arrays.copyOf(embedding, embedding.length); diff --git a/vector-stores/spring-ai-azure-cosmos-db-store/src/main/java/org/springframework/ai/vectorstore/cosmosdb/CosmosDBVectorStore.java b/vector-stores/spring-ai-azure-cosmos-db-store/src/main/java/org/springframework/ai/vectorstore/cosmosdb/CosmosDBVectorStore.java index 3593fd8e9f..b406f53411 100644 --- a/vector-stores/spring-ai-azure-cosmos-db-store/src/main/java/org/springframework/ai/vectorstore/cosmosdb/CosmosDBVectorStore.java +++ b/vector-stores/spring-ai-azure-cosmos-db-store/src/main/java/org/springframework/ai/vectorstore/cosmosdb/CosmosDBVectorStore.java @@ -52,14 +52,14 @@ import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.models.ThroughputProperties; import com.azure.cosmos.util.CosmosPagedFlux; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.lang3.tuple.ImmutablePair; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -193,17 +193,15 @@ public void close() { } private JsonNode mapCosmosDocument(Document document, float[] queryEmbedding) { - ObjectMapper objectMapper = new ObjectMapper(); - String id = document.getId(); String content = document.getText(); // Convert metadata and embedding directly to JsonNode - JsonNode metadataNode = objectMapper.valueToTree(document.getMetadata()); - JsonNode embeddingNode = objectMapper.valueToTree(queryEmbedding); + JsonNode metadataNode = JsonMapper.shared().valueToTree(document.getMetadata()); + JsonNode embeddingNode = JsonMapper.shared().valueToTree(queryEmbedding); // Create an ObjectNode specifically - ObjectNode objectNode = objectMapper.createObjectNode(); + ObjectNode objectNode = JsonMapper.shared().createObjectNode(); // Use put for simple values and set for JsonNode values objectNode.put("id", id); @@ -421,11 +419,11 @@ public List doSimilaritySearch(SearchRequest request) { Map docFields = new HashMap<>(); for (var doc : documents) { JsonNode metadata = doc.get("metadata"); - metadata.fieldNames().forEachRemaining(field -> { - JsonNode value = metadata.get(field); + metadata.propertyNames().forEach(property -> { + JsonNode value = metadata.get(property); Object parsedValue = value.isTextual() ? value.asText() : value.isNumber() ? value.numberValue() : value.isBoolean() ? value.booleanValue() : value.toString(); - docFields.put(field, parsedValue); + docFields.put(property, parsedValue); }); } diff --git a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaApi.java b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaApi.java index bb67e95161..8ae452c884 100644 --- a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaApi.java +++ b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaApi.java @@ -25,10 +25,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import org.jspecify.annotations.Nullable; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chroma.vectorstore.ChromaApi.QueryRequest.Include; import org.springframework.ai.chroma.vectorstore.common.ChromaApiConstants; @@ -65,18 +64,18 @@ public static Builder builder() { private static final String X_CHROMA_TOKEN_NAME = "x-chroma-token"; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private RestClient restClient; private @Nullable String keyToken; - ChromaApi(String baseUrl, RestClient.Builder restClientBuilder, ObjectMapper objectMapper) { + public ChromaApi(String baseUrl, RestClient.Builder restClientBuilder, JsonMapper jsonMapper) { this.restClient = restClientBuilder.baseUrl(baseUrl) .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) .build(); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; } /** @@ -307,13 +306,8 @@ public int deleteEmbeddings(String tenantName, String databaseName, String colle // Utils public Map where(String text) { - try { - return this.objectMapper.readValue(text, new TypeReference>() { - }); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return this.jsonMapper.readValue(text, new TypeReference<>() { + }); } private void httpHeaders(HttpHeaders headers) { @@ -624,7 +618,7 @@ public static final class Builder { private RestClient.Builder restClientBuilder = RestClient.builder(); - private ObjectMapper objectMapper = new ObjectMapper(); + @Nullable private JsonMapper jsonMapper; public Builder baseUrl(String baseUrl) { Assert.hasText(baseUrl, "baseUrl cannot be null or empty"); @@ -638,14 +632,15 @@ public Builder restClientBuilder(RestClient.Builder restClientBuilder) { return this; } - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "objectMapper cannot be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(JsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "jsonMapper cannot be null"); + this.jsonMapper = jsonMapper; return this; } public ChromaApi build() { - return new ChromaApi(this.baseUrl, this.restClientBuilder, this.objectMapper); + return new ChromaApi(this.baseUrl, this.restClientBuilder, + (this.jsonMapper != null ? this.jsonMapper : new JsonMapper())); } } diff --git a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStore.java b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStore.java index 658e93be68..d6781e9d94 100644 --- a/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStore.java +++ b/vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStore.java @@ -21,12 +21,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.chroma.vectorstore.ChromaApi.AddEmbeddingsRequest; import org.springframework.ai.chroma.vectorstore.ChromaApi.DeleteEmbeddingsRequest; @@ -79,7 +77,7 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements private final boolean initializeSchema; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private boolean initialized = false; @@ -97,7 +95,7 @@ protected ChromaVectorStore(Builder builder) { this.collectionName = builder.collectionName; this.initializeSchema = builder.initializeSchema; this.filterExpressionConverter = builder.filterExpressionConverter; - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); if (builder.initializeImmediately) { try { @@ -243,12 +241,7 @@ public List doSimilaritySearch(SearchRequest request) { @SuppressWarnings("unchecked") private Map jsonToMap(String jsonText) { - try { - return (Map) this.objectMapper.readValue(jsonText, Map.class); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return (Map) this.jsonMapper.readValue(jsonText, Map.class); } @Override diff --git a/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStore.java b/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStore.java index 85b9735177..2e353d48f4 100644 --- a/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStore.java +++ b/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStore.java @@ -30,12 +30,10 @@ import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.search.Hit; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.json.jackson.Jackson3JsonpMapper; import co.elastic.clients.transport.Version; import co.elastic.clients.transport.rest5_client.Rest5ClientTransport; import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import org.jspecify.annotations.Nullable; import org.springframework.ai.document.Document; @@ -169,9 +167,8 @@ protected ElasticsearchVectorStore(Builder builder) { this.filterExpressionConverter = builder.filterExpressionConverter; String version = Version.VERSION == null ? "Unknown" : Version.VERSION.toString(); - this.elasticsearchClient = new ElasticsearchClient(new Rest5ClientTransport(builder.restClient, - new JacksonJsonpMapper( - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)))) + this.elasticsearchClient = new ElasticsearchClient( + new Rest5ClientTransport(builder.restClient, new Jackson3JsonpMapper())) .withTransportOptions(t -> t.addHeader("user-agent", "spring-ai elastic-java/" + version)); } diff --git a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreObservationIT.java b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreObservationIT.java index 9b7b5dd31f..b8475bc876 100644 --- a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreObservationIT.java +++ b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreObservationIT.java @@ -229,7 +229,7 @@ Rest5Client restClient() throws URISyntaxException { @Bean ElasticsearchClient elasticsearchClient(Rest5Client restClient) { return new ElasticsearchClient(new Rest5ClientTransport(restClient, new JacksonJsonpMapper( - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)))); + new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)))); } } diff --git a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java index 2f81efc33e..5d971eabc1 100644 --- a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java +++ b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java @@ -24,12 +24,10 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; @@ -104,7 +102,7 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement private final boolean initializeSchema; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private final String indexName; @@ -160,7 +158,7 @@ else if (builder.isUsingBasicAuthentication()) { this.client = webClientBuilder.build(); this.filterExpressionConverter = new GemFireAiSearchFilterExpressionConverter(); - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } public static Builder builder(EmbeddingModel embeddingModel) { @@ -233,14 +231,8 @@ public void doAdd(List documents) { DOCUMENT_FIELD, Objects.requireNonNullElse(document.getText(), ""), document.getMetadata())) .toList()); - String embeddingsJson = null; - try { - String embeddingString = this.objectMapper.writeValueAsString(upload); - embeddingsJson = embeddingString.substring("{\"embeddings\":".length()); - } - catch (JsonProcessingException e) { - throw new RuntimeException(String.format("Embedding JSON parsing error: %s", e.getMessage())); - } + String embeddingString = this.jsonMapper.writeValueAsString(upload); + String embeddingsJson = embeddingString.substring("{\"embeddings\":".length()); this.client.post() .uri("/" + this.indexName + EMBEDDINGS) @@ -262,7 +254,7 @@ public void doDelete(List idList) { .bodyToMono(Void.class) .block(); } - catch (Exception e) { + catch (RuntimeException e) { logger.warn("Error removing embedding: {}", e.getMessage(), e); } } @@ -302,13 +294,12 @@ public List doSimilaritySearch(SearchRequest request) { /** * Creates a new index in the GemFireVectorStore using specified parameters. This * method is invoked during initialization. - * @throws JsonProcessingException if an error occurs during JSON processing */ - public void createIndex() throws JsonProcessingException { + public void createIndex() { CreateRequest createRequest = new CreateRequest(this.indexName, this.beamWidth, this.maxConnections, this.vectorSimilarityFunction, this.fields, this.buckets); - String index = this.objectMapper.writeValueAsString(createRequest); + String index = this.jsonMapper.writeValueAsString(createRequest); this.client.post() .contentType(MediaType.APPLICATION_JSON) diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/hanadb/HanaCloudVectorStore.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/hanadb/HanaCloudVectorStore.java index 7184de4242..bc0494135f 100644 --- a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/hanadb/HanaCloudVectorStore.java +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/hanadb/HanaCloudVectorStore.java @@ -20,12 +20,10 @@ import java.util.List; import java.util.stream.Collectors; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -84,7 +82,7 @@ public class HanaCloudVectorStore extends AbstractObservationVectorStore { private final int topK; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; /** * Protected constructor that accepts a builder instance. This is the preferred way to @@ -99,7 +97,7 @@ protected HanaCloudVectorStore(Builder builder) { this.repository = builder.repository; this.tableName = builder.tableName; this.topK = builder.topK; - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } /** @@ -161,14 +159,9 @@ public List doSimilaritySearch(SearchRequest request) { logger.info("Hana cosine-similarity for query={}, with topK={} returned {} results", request.getQuery(), request.getTopK(), searchResult.size()); - return searchResult.stream().map(c -> { - try { - return new Document(c.get_id(), this.objectMapper.writeValueAsString(c), Collections.emptyMap()); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }).collect(Collectors.toList()); + return searchResult.stream() + .map(c -> new Document(c.get_id(), this.jsonMapper.writeValueAsString(c), Collections.emptyMap())) + .collect(Collectors.toList()); } private String getEmbedding(SearchRequest searchRequest) { diff --git a/vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/ai/vectorstore/mariadb/MariaDBVectorStore.java b/vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/ai/vectorstore/mariadb/MariaDBVectorStore.java index 871c995078..0b7f5b60bd 100644 --- a/vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/ai/vectorstore/mariadb/MariaDBVectorStore.java +++ b/vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/ai/vectorstore/mariadb/MariaDBVectorStore.java @@ -25,12 +25,10 @@ import java.util.Map; import java.util.Optional; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -187,7 +185,7 @@ public class MariaDBVectorStore extends AbstractObservationVectorStore implement private final MariaDBDistanceType distanceType; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private final boolean removeExistingVectorStoreTable; @@ -208,7 +206,7 @@ protected MariaDBVectorStore(MariaDBBuilder builder) { Assert.notNull(builder.jdbcTemplate, "JdbcTemplate must not be null"); - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); this.vectorTableName = builder.vectorTableName.isEmpty() ? DEFAULT_TABLE_NAME : MariaDBSchemaValidator.validateAndEnquoteIdentifier(builder.vectorTableName.trim(), false); @@ -307,12 +305,7 @@ public int getBatchSize() { } private String toJson(Map map) { - try { - return this.objectMapper.writeValueAsString(map); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return this.jsonMapper.writeValueAsString(map); } @Override @@ -366,7 +359,7 @@ public List doSimilaritySearch(SearchRequest request) { logger.debug("SQL query: {}", sql); - return this.jdbcTemplate.query(sql, new DocumentRowMapper(this.objectMapper), embedding, distance, + return this.jdbcTemplate.query(sql, new DocumentRowMapper(this.jsonMapper), embedding, distance, request.getTopK()); } @@ -479,10 +472,10 @@ public enum MariaDBDistanceType { private static class DocumentRowMapper implements RowMapper { - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; - DocumentRowMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + DocumentRowMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; } @Override @@ -504,12 +497,7 @@ public Document mapRow(ResultSet rs, int rowNum) throws SQLException { } private Map toMap(String source) { - try { - return (Map) this.objectMapper.readValue(source, Map.class); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return (Map) this.jsonMapper.readValue(source, Map.class); } } diff --git a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/pgvector/PgVectorStore.java b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/pgvector/PgVectorStore.java index 05f644bd3a..b5397ccf50 100644 --- a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/pgvector/PgVectorStore.java +++ b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/pgvector/PgVectorStore.java @@ -25,13 +25,11 @@ import java.util.Optional; import java.util.UUID; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.pgvector.PGvector; import org.postgresql.util.PGobject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; @@ -201,7 +199,7 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini private final PgDistanceType distanceType; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; private final DocumentRowMapper documentRowMapper; @@ -221,8 +219,8 @@ protected PgVectorStore(PgVectorStoreBuilder builder) { Assert.notNull(builder.jdbcTemplate, "JdbcTemplate must not be null"); - this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); - this.documentRowMapper = new DocumentRowMapper(this.objectMapper); + this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); + this.documentRowMapper = new DocumentRowMapper(this.jsonMapper); String vectorTable = builder.vectorTableName; this.vectorTableName = vectorTable.isEmpty() ? DEFAULT_TABLE_NAME : vectorTable.trim(); @@ -305,12 +303,7 @@ public int getBatchSize() { } private String toJson(Map map) { - try { - return this.objectMapper.writeValueAsString(map); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return this.jsonMapper.writeValueAsString(map); } private Object convertIdToPgType(String id) { @@ -609,10 +602,10 @@ private static class DocumentRowMapper implements RowMapper { private static final String COLUMN_DISTANCE = "distance"; - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; - DocumentRowMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + DocumentRowMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; } @Override @@ -637,12 +630,7 @@ public Document mapRow(ResultSet rs, int rowNum) throws SQLException { private Map toMap(PGobject pgObject) { String source = pgObject.getValue(); - try { - return (Map) this.objectMapper.readValue(source, Map.class); - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return (Map) this.jsonMapper.readValue(source, Map.class); } } diff --git a/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStore.java b/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStore.java index 422508caac..a277c1c049 100644 --- a/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStore.java +++ b/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStore.java @@ -23,8 +23,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.Struct; import com.google.protobuf.Value; import com.google.protobuf.util.JsonFormat; @@ -35,6 +33,8 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; @@ -79,8 +79,6 @@ public class PineconeVectorStore extends AbstractObservationVectorStore { private final Pinecone pinecone; - private final ObjectMapper objectMapper; - private static final Logger logger = LoggerFactory.getLogger(PineconeVectorStore.class); /** @@ -99,7 +97,6 @@ protected PineconeVectorStore(Builder builder) { this.pineconeDistanceMetadataFieldName = builder.distanceMetadataFieldName; this.pinecone = new Pinecone.Builder(builder.apiKey).build(); - this.objectMapper = new ObjectMapper(); } /** @@ -168,7 +165,7 @@ private Struct metadataToStruct(Document document) { var structBuilder = Struct.newBuilder(); JsonFormat.parser() .ignoringUnknownFields() - .merge(this.objectMapper.writeValueAsString(document.getMetadata()), structBuilder); + .merge(JsonMapper.shared().writeValueAsString(document.getMetadata()), structBuilder); structBuilder.putFields(this.pineconeContentFieldName, contentValue(document)); return structBuilder.build(); } @@ -299,7 +296,7 @@ private Struct metadataFiltersToStruct(String metadataFilters) { private Map extractMetadata(Struct metadataStruct) { try { String json = JsonFormat.printer().print(metadataStruct); - Map metadata = this.objectMapper.readValue(json, new TypeReference<>() { + Map metadata = JsonMapper.shared().readValue(json, new TypeReference<>() { }); metadata.remove(this.pineconeContentFieldName); diff --git a/vector-stores/spring-ai-weaviate-store/src/main/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStore.java b/vector-stores/spring-ai-weaviate-store/src/main/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStore.java index 6665eb1120..aed5574893 100644 --- a/vector-stores/spring-ai-weaviate-store/src/main/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStore.java +++ b/vector-stores/spring-ai-weaviate-store/src/main/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStore.java @@ -25,8 +25,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.weaviate.client.WeaviateClient; import io.weaviate.client.base.Result; import io.weaviate.client.base.WeaviateErrorMessage; @@ -46,6 +44,8 @@ import io.weaviate.client.v1.graphql.query.fields.Fields; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; @@ -135,12 +135,6 @@ public class WeaviateVectorStore extends AbstractObservationVectorStore { */ private final WeaviateFilterExpressionConverter filterExpressionConverter; - /** - * Used to serialize/deserialize the document metadata when stored/retrieved from the - * weaviate vector store. - */ - private final ObjectMapper objectMapper = new ObjectMapper(); - /** * Protected constructor for creating a WeaviateVectorStore instance using the builder * pattern. This constructor initializes the vector store with the configured settings @@ -249,10 +243,10 @@ private WeaviateObject toWeaviateObject(Document document, List docume Map fields = new HashMap<>(); fields.put(this.options.getContentFieldName(), document.getText()); try { - String metadataString = this.objectMapper.writeValueAsString(document.getMetadata()); + String metadataString = JsonMapper.shared().writeValueAsString(document.getMetadata()); fields.put(METADATA_FIELD_NAME, metadataString); } - catch (JsonProcessingException e) { + catch (JacksonException e) { throw new RuntimeException("Failed to serialize the Document metadata: " + document.getText()); } @@ -323,7 +317,7 @@ protected void doDelete(Filter.Expression filterExpression) { logger.debug("No documents found matching filter expression"); } } - catch (Exception e) { + catch (JacksonException e) { logger.error("Failed to delete documents by filter", e); throw new IllegalStateException("Failed to delete documents by filter", e); } @@ -411,14 +405,9 @@ private Document toDocument(Map item) { Map metadata = new HashMap<>(); metadata.put(DocumentMetadata.DISTANCE.value(), 1 - certainty); - try { - String metadataJson = (String) item.get(METADATA_FIELD_NAME); - if (StringUtils.hasText(metadataJson)) { - metadata.putAll(this.objectMapper.readValue(metadataJson, Map.class)); - } - } - catch (Exception e) { - throw new RuntimeException(e); + String metadataJson = (String) item.get(METADATA_FIELD_NAME); + if (StringUtils.hasText(metadataJson)) { + metadata.putAll(JsonMapper.shared().readValue(metadataJson, Map.class)); } // Content