Skip to content

Commit d229f4e

Browse files
committed
Refine Jackson ObjectMapper handling
ObjectMapper instantiation is costly, so unless its usage is one-shot, it is better to create a reusable instance for upcoming usage. Also, before this commit, serialization of most Kotlin classes was not supported due to the lack of proper Jackson KotlinModule detection. This commit: - Avoids per invocation ObjectMapper instantiation when relevant - Automatically detects and enables well-known Jackson modules including the Kotlin one - Removes org.springframework.ai.vectorstore.JsonUtils which looks not needed anymore More optimizations are possible like reusing more ObjectMapper instances, but this could introduce more breaking changes so this commit intends to be a good first step. Kotlin tests will be provided in a follow-up commit.
1 parent c979238 commit d229f4e

File tree

24 files changed

+132
-146
lines changed

24 files changed

+132
-146
lines changed

models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiImageModel.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import com.fasterxml.jackson.databind.DeserializationFeature;
1111
import com.fasterxml.jackson.databind.ObjectMapper;
1212
import com.fasterxml.jackson.databind.SerializationFeature;
13-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
13+
import com.fasterxml.jackson.databind.json.JsonMapper;
1414
import org.slf4j.Logger;
1515
import org.slf4j.LoggerFactory;
1616
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata;
@@ -22,6 +22,7 @@
2222
import org.springframework.ai.image.ImageResponse;
2323
import org.springframework.ai.image.ImageResponseMetadata;
2424
import org.springframework.ai.model.ModelOptionsUtils;
25+
import org.springframework.ai.util.JacksonUtils;
2526
import org.springframework.util.Assert;
2627

2728
import java.util.List;
@@ -47,6 +48,8 @@ public class AzureOpenAiImageModel implements ImageModel {
4748

4849
private final AzureOpenAiImageOptions defaultOptions;
4950

51+
private final ObjectMapper objectMapper;
52+
5053
public AzureOpenAiImageModel(OpenAIClient openAIClient) {
5154
this(openAIClient, AzureOpenAiImageOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
5255
}
@@ -56,6 +59,11 @@ public AzureOpenAiImageModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiImag
5659
Assert.notNull(options, "AzureOpenAiChatOptions must not be null");
5760
this.openAIClient = microsoftOpenAiClient;
5861
this.defaultOptions = options;
62+
this.objectMapper = JsonMapper.builder()
63+
.addModules(JacksonUtils.instantiateAvailableModules())
64+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
65+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
66+
.build();
5967
}
6068

6169
public AzureOpenAiImageOptions getDefaultOptions() {
@@ -88,11 +96,8 @@ public ImageResponse call(ImagePrompt imagePrompt) {
8896
}
8997

9098
private String toPrettyJson(Object object) {
91-
ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
92-
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
93-
.registerModule(new JavaTimeModule());
9499
try {
95-
return objectMapper.writeValueAsString(object);
100+
return this.objectMapper.writeValueAsString(object);
96101
}
97102
catch (JsonProcessingException e) {
98103
return "JsonProcessingException:" + e + " [" + object.toString() + "]";

models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/api/VertexAiPaLm2Api.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import com.fasterxml.jackson.annotation.JsonInclude.Include;
2626
import com.fasterxml.jackson.annotation.JsonProperty;
2727
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.fasterxml.jackson.databind.json.JsonMapper;
2829

30+
import org.springframework.ai.util.JacksonUtils;
2931
import org.springframework.http.HttpHeaders;
3032
import org.springframework.http.MediaType;
3133
import org.springframework.http.client.ClientHttpResponse;
@@ -116,6 +118,8 @@ public class VertexAiPaLm2Api {
116118

117119
private final String embeddingModel;
118120

121+
private final ObjectMapper objectMapper;
122+
119123
/**
120124
* Create a new chat completion api.
121125
* @param apiKey vertex apiKey.
@@ -138,6 +142,7 @@ public VertexAiPaLm2Api(String baseUrl, String apiKey, String model, String embe
138142
this.chatModel = model;
139143
this.embeddingModel = embeddingModel;
140144
this.apiKey = apiKey;
145+
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
141146

142147
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
143148
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
@@ -154,7 +159,7 @@ public boolean hasError(ClientHttpResponse response) throws IOException {
154159
public void handleError(ClientHttpResponse response) throws IOException {
155160
if (response.getStatusCode().isError()) {
156161
throw new RuntimeException(String.format("%s - %s", response.getStatusCode().value(),
157-
new ObjectMapper().readValue(response.getBody(), ResponseError.class)));
162+
objectMapper.readValue(response.getBody(), ResponseError.class)));
158163
}
159164
}
160165
};

spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import java.lang.reflect.Type;
2222
import java.util.Objects;
2323

24+
import com.fasterxml.jackson.databind.json.JsonMapper;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
27+
28+
import org.springframework.ai.util.JacksonUtils;
2629
import org.springframework.core.ParameterizedTypeReference;
2730
import org.springframework.lang.NonNull;
2831

@@ -34,7 +37,6 @@
3437
import com.fasterxml.jackson.databind.JsonNode;
3538
import com.fasterxml.jackson.databind.ObjectMapper;
3639
import com.fasterxml.jackson.databind.ObjectWriter;
37-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3840
import com.github.victools.jsonschema.generator.Option;
3941
import com.github.victools.jsonschema.generator.SchemaGenerator;
4042
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
@@ -65,12 +67,10 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
6567
/**
6668
* The target class type reference to which the output will be converted.
6769
*/
68-
@SuppressWarnings({ "FieldMayBeFinal" })
69-
private TypeReference<T> typeRef;
70+
private final TypeReference<T> typeRef;
7071

7172
/** The object mapper used for deserialization and other JSON operations. */
72-
@SuppressWarnings("FieldMayBeFinal")
73-
private ObjectMapper objectMapper;
73+
private final ObjectMapper objectMapper;
7474

7575
/**
7676
* Constructor to initialize with the target type's class.
@@ -149,7 +149,7 @@ private void generateSchema() {
149149
SchemaGeneratorConfig config = configBuilder.build();
150150
SchemaGenerator generator = new SchemaGenerator(config);
151151
JsonNode jsonNode = generator.generateSchema(this.typeRef.getType());
152-
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
152+
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
153153
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
154154
try {
155155
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
@@ -201,10 +201,10 @@ public T convert(@NonNull String text) {
201201
* @return Configured object mapper.
202202
*/
203203
protected ObjectMapper getObjectMapper() {
204-
ObjectMapper mapper = new ObjectMapper();
205-
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
206-
mapper.registerModule(new JavaTimeModule());
207-
return mapper;
204+
return JsonMapper.builder()
205+
.addModules(JacksonUtils.instantiateAvailableModules())
206+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
207+
.build();
208208
}
209209

210210
/**

spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
import org.springframework.ai.chat.model.ToolContext;
2222
import org.springframework.ai.model.ModelOptionsUtils;
2323
import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType;
24+
import org.springframework.ai.util.JacksonUtils;
2425
import org.springframework.util.Assert;
2526

2627
import com.fasterxml.jackson.databind.DeserializationFeature;
2728
import com.fasterxml.jackson.databind.ObjectMapper;
2829
import com.fasterxml.jackson.databind.SerializationFeature;
29-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
30+
import com.fasterxml.jackson.databind.json.JsonMapper;
3031

3132
/**
3233
* Note that the underlying function is responsible for converting the output into format
@@ -90,10 +91,7 @@ public Builder(Function<I, O> function) {
9091

9192
private String inputTypeSchema;
9293

93-
private ObjectMapper objectMapper = new ObjectMapper()
94-
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
95-
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
96-
.registerModule(new JavaTimeModule());
94+
private ObjectMapper objectMapper;
9795

9896
public Builder<I, O> withName(String name) {
9997
Assert.hasText(name, "Name must not be empty");
@@ -142,7 +140,14 @@ public FunctionCallbackWrapper<I, O> build() {
142140
Assert.hasText(this.name, "Name must not be empty");
143141
Assert.hasText(this.description, "Description must not be empty");
144142
Assert.notNull(this.responseConverter, "ResponseConverter must not be null");
145-
Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
143+
144+
if (this.objectMapper == null) {
145+
this.objectMapper = JsonMapper.builder()
146+
.addModules(JacksonUtils.instantiateAvailableModules())
147+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
148+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
149+
.build();
150+
}
146151

147152
if (this.inputType == null) {
148153
if (this.function != null) {

spring-ai-core/src/main/java/org/springframework/ai/parser/BeanOutputParser.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.databind.JsonNode;
2323
import com.fasterxml.jackson.databind.ObjectMapper;
2424
import com.fasterxml.jackson.databind.ObjectWriter;
25+
import com.fasterxml.jackson.databind.json.JsonMapper;
2526
import com.github.victools.jsonschema.generator.SchemaGenerator;
2627
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
2728
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
@@ -30,6 +31,8 @@
3031
import java.util.Map;
3132
import java.util.Objects;
3233

34+
import org.springframework.ai.util.JacksonUtils;
35+
3336
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
3437
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
3538

@@ -91,7 +94,7 @@ private void generateSchema() {
9194
SchemaGeneratorConfig config = configBuilder.build();
9295
SchemaGenerator generator = new SchemaGenerator(config);
9396
JsonNode jsonNode = generator.generateSchema(this.clazz);
94-
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
97+
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
9598
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
9699
try {
97100
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
@@ -142,9 +145,10 @@ private String jsonSchemaToInstance(String text) {
142145
* @return Configured object mapper.
143146
*/
144147
protected ObjectMapper getObjectMapper() {
145-
ObjectMapper mapper = new ObjectMapper();
146-
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
147-
return mapper;
148+
return JsonMapper.builder()
149+
.addModules(JacksonUtils.instantiateAvailableModules())
150+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
151+
.build();
148152
}
149153

150154
/**

spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
import java.util.Optional;
3333
import java.util.concurrent.ConcurrentHashMap;
3434

35+
import com.fasterxml.jackson.databind.json.JsonMapper;
3536
import org.slf4j.Logger;
3637
import org.slf4j.LoggerFactory;
3738
import org.springframework.ai.document.Document;
3839
import org.springframework.ai.embedding.EmbeddingModel;
3940
import org.springframework.ai.observation.conventions.VectorStoreProvider;
4041
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
42+
import org.springframework.ai.util.JacksonUtils;
4143
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
4244
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
4345
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
@@ -69,6 +71,8 @@ public class SimpleVectorStore extends AbstractObservationVectorStore {
6971

7072
private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class);
7173

74+
private final ObjectMapper objectMapper;
75+
7276
protected Map<String, Document> store = new ConcurrentHashMap<>();
7377

7478
protected EmbeddingModel embeddingModel;
@@ -84,6 +88,7 @@ public SimpleVectorStore(EmbeddingModel embeddingModel, ObservationRegistry obse
8488

8589
Objects.requireNonNull(embeddingModel, "EmbeddingModel must not be null");
8690
this.embeddingModel = embeddingModel;
91+
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
8792
}
8893

8994
@Override
@@ -172,9 +177,8 @@ public void save(File file) {
172177
public void load(File file) {
173178
TypeReference<HashMap<String, Document>> typeRef = new TypeReference<>() {
174179
};
175-
ObjectMapper objectMapper = new ObjectMapper();
176180
try {
177-
Map<String, Document> deserializedMap = objectMapper.readValue(file, typeRef);
181+
Map<String, Document> deserializedMap = this.objectMapper.readValue(file, typeRef);
178182
this.store = deserializedMap;
179183
}
180184
catch (IOException ex) {
@@ -189,9 +193,8 @@ public void load(File file) {
189193
public void load(Resource resource) {
190194
TypeReference<HashMap<String, Document>> typeRef = new TypeReference<>() {
191195
};
192-
ObjectMapper objectMapper = new ObjectMapper();
193196
try {
194-
Map<String, Document> deserializedMap = objectMapper.readValue(resource.getInputStream(), typeRef);
197+
Map<String, Document> deserializedMap = this.objectMapper.readValue(resource.getInputStream(), typeRef);
195198
this.store = deserializedMap;
196199
}
197200
catch (IOException ex) {
@@ -200,8 +203,7 @@ public void load(Resource resource) {
200203
}
201204

202205
private String getVectorDbAsJson() {
203-
ObjectMapper objectMapper = new ObjectMapper();
204-
ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
206+
ObjectWriter objectWriter = this.objectMapper.writerWithDefaultPrettyPrinter();
205207
String json;
206208
try {
207209
json = objectWriter.writeValueAsString(this.store);

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic/BedrockAnthropicChatAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ public class BedrockAnthropicChatAutoConfiguration {
5252
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
5353
public AnthropicChatBedrockApi anthropicApi(AwsCredentialsProvider credentialsProvider,
5454
AwsRegionProvider regionProvider, BedrockAnthropicChatProperties properties,
55-
BedrockAwsConnectionProperties awsProperties) {
55+
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
5656
return new AnthropicChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
57-
new ObjectMapper(), awsProperties.getTimeout());
57+
objectMapper, awsProperties.getTimeout());
5858
}
5959

6060
@Bean

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/anthropic3/BedrockAnthropic3ChatAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ public class BedrockAnthropic3ChatAutoConfiguration {
5252
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
5353
public Anthropic3ChatBedrockApi anthropic3Api(AwsCredentialsProvider credentialsProvider,
5454
AwsRegionProvider regionProvider, BedrockAnthropic3ChatProperties properties,
55-
BedrockAwsConnectionProperties awsProperties) {
55+
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
5656
return new Anthropic3ChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
57-
new ObjectMapper(), awsProperties.getTimeout());
57+
objectMapper, awsProperties.getTimeout());
5858
}
5959

6060
@Bean

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereChatAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ public class BedrockCohereChatAutoConfiguration {
5050
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
5151
public CohereChatBedrockApi cohereChatApi(AwsCredentialsProvider credentialsProvider,
5252
AwsRegionProvider regionProvider, BedrockCohereChatProperties properties,
53-
BedrockAwsConnectionProperties awsProperties) {
53+
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
5454
return new CohereChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
55-
new ObjectMapper(), awsProperties.getTimeout());
55+
objectMapper, awsProperties.getTimeout());
5656
}
5757

5858
@Bean

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public class BedrockCohereEmbeddingAutoConfiguration {
5151
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
5252
public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider,
5353
AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties,
54-
BedrockAwsConnectionProperties awsProperties) {
54+
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
5555
return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
56-
new ObjectMapper(), awsProperties.getTimeout());
56+
objectMapper, awsProperties.getTimeout());
5757
}
5858

5959
@Bean

0 commit comments

Comments
 (0)