Skip to content

Commit f21b8a4

Browse files
sdeleuzetzolov
authored andcommitted
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. Additional changes: - Update ModelOptionsUtils to use JacksonUtils.instantiateAvailableModules() - Add missing license headers - Add missing author Javadoc comments
1 parent 9f6ca77 commit f21b8a4

File tree

26 files changed

+244
-155
lines changed

26 files changed

+244
-155
lines changed

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

Lines changed: 11 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;
@@ -33,6 +34,7 @@
3334
* {@link OpenAIClient}.
3435
*
3536
* @author Benoit Moussaud
37+
* @author Sebastien Deleuze
3638
* @see ImageModel
3739
* @see com.azure.ai.openai.OpenAIClient
3840
* @since 1.0.0
@@ -47,6 +49,8 @@ public class AzureOpenAiImageModel implements ImageModel {
4749

4850
private final AzureOpenAiImageOptions defaultOptions;
4951

52+
private final ObjectMapper objectMapper;
53+
5054
public AzureOpenAiImageModel(OpenAIClient openAIClient) {
5155
this(openAIClient, AzureOpenAiImageOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
5256
}
@@ -56,6 +60,11 @@ public AzureOpenAiImageModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiImag
5660
Assert.notNull(options, "AzureOpenAiChatOptions must not be null");
5761
this.openAIClient = microsoftOpenAiClient;
5862
this.defaultOptions = options;
63+
this.objectMapper = JsonMapper.builder()
64+
.addModules(JacksonUtils.instantiateAvailableModules())
65+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
66+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
67+
.build();
5968
}
6069

6170
public AzureOpenAiImageOptions getDefaultOptions() {
@@ -88,11 +97,8 @@ public ImageResponse call(ImagePrompt imagePrompt) {
8897
}
8998

9099
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());
94100
try {
95-
return objectMapper.writeValueAsString(object);
101+
return this.objectMapper.writeValueAsString(object);
96102
}
97103
catch (JsonProcessingException e) {
98104
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: 11 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;
@@ -54,6 +56,7 @@
5456
* @author Sebastian Ullrich
5557
* @author Kirk Lund
5658
* @author Josh Long
59+
* @author Sebastien Deleuze
5760
*/
5861
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
5962

@@ -65,12 +68,10 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
6568
/**
6669
* The target class type reference to which the output will be converted.
6770
*/
68-
@SuppressWarnings({ "FieldMayBeFinal" })
69-
private TypeReference<T> typeRef;
71+
private final TypeReference<T> typeRef;
7072

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

7576
/**
7677
* Constructor to initialize with the target type's class.
@@ -149,7 +150,7 @@ private void generateSchema() {
149150
SchemaGeneratorConfig config = configBuilder.build();
150151
SchemaGenerator generator = new SchemaGenerator(config);
151152
JsonNode jsonNode = generator.generateSchema(this.typeRef.getType());
152-
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
153+
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
153154
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
154155
try {
155156
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
@@ -201,10 +202,10 @@ public T convert(@NonNull String text) {
201202
* @return Configured object mapper.
202203
*/
203204
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;
205+
return JsonMapper.builder()
206+
.addModules(JacksonUtils.instantiateAvailableModules())
207+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
208+
.build();
208209
}
209210

210211
/**

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,23 @@
2626
import java.util.concurrent.atomic.AtomicReference;
2727
import java.util.stream.Collectors;
2828

29+
import org.springframework.ai.util.JacksonUtils;
30+
import org.springframework.beans.BeanWrapper;
31+
import org.springframework.beans.BeanWrapperImpl;
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.CollectionUtils;
34+
import org.springframework.util.ObjectUtils;
35+
2936
import com.fasterxml.jackson.annotation.JsonProperty;
3037
import com.fasterxml.jackson.core.JsonProcessingException;
3138
import com.fasterxml.jackson.core.type.TypeReference;
3239
import com.fasterxml.jackson.databind.DeserializationFeature;
3340
import com.fasterxml.jackson.databind.JsonNode;
3441
import com.fasterxml.jackson.databind.ObjectMapper;
3542
import com.fasterxml.jackson.databind.SerializationFeature;
43+
import com.fasterxml.jackson.databind.json.JsonMapper;
3644
import com.fasterxml.jackson.databind.node.ArrayNode;
3745
import com.fasterxml.jackson.databind.node.ObjectNode;
38-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3946
import com.github.victools.jsonschema.generator.Option;
4047
import com.github.victools.jsonschema.generator.OptionPreset;
4148
import com.github.victools.jsonschema.generator.SchemaGenerator;
@@ -46,12 +53,6 @@
4653
import com.github.victools.jsonschema.module.jackson.JacksonOption;
4754
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
4855

49-
import org.springframework.beans.BeanWrapper;
50-
import org.springframework.beans.BeanWrapperImpl;
51-
import org.springframework.util.Assert;
52-
import org.springframework.util.CollectionUtils;
53-
import org.springframework.util.ObjectUtils;
54-
5556
/**
5657
* Utility class for manipulating {@link ModelOptions} objects.
5758
*
@@ -61,10 +62,11 @@
6162
*/
6263
public abstract class ModelOptionsUtils {
6364

64-
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
65+
public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
6566
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
6667
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
67-
.registerModule(new JavaTimeModule());
68+
.addModules(JacksonUtils.instantiateAvailableModules())
69+
.build();
6870

6971
private static final List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");
7072

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,22 @@
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
3334
* that can be consumed by the Model. The default implementation converts the output into
3435
* String before sending it to the Model. Provide a custom function responseConverter
3536
* implementation to override this.
37+
*
38+
* @author Christian Tzolov
39+
* @author Sebastien Deleuze
3640
*
3741
*/
3842
public class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O> {
@@ -90,10 +94,7 @@ public Builder(Function<I, O> function) {
9094

9195
private String inputTypeSchema;
9296

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

9899
public Builder<I, O> withName(String name) {
99100
Assert.hasText(name, "Name must not be empty");
@@ -142,7 +143,14 @@ public FunctionCallbackWrapper<I, O> build() {
142143
Assert.hasText(this.name, "Name must not be empty");
143144
Assert.hasText(this.description, "Description must not be empty");
144145
Assert.notNull(this.responseConverter, "ResponseConverter must not be null");
145-
Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
146+
147+
if (this.objectMapper == null) {
148+
this.objectMapper = JsonMapper.builder()
149+
.addModules(JacksonUtils.instantiateAvailableModules())
150+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
151+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
152+
.build();
153+
}
146154

147155
if (this.inputType == null) {
148156
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
/**
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2024 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.util;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
import com.fasterxml.jackson.databind.Module;
22+
23+
import org.springframework.beans.BeanUtils;
24+
import org.springframework.core.KotlinDetector;
25+
import org.springframework.util.ClassUtils;
26+
27+
/**
28+
* Utility methods for Jackson.
29+
*
30+
* @author Sebastien Deleuze
31+
*/
32+
public abstract class JacksonUtils {
33+
34+
/**
35+
* Instantiate well-known Jackson modules available in the classpath.
36+
* <p>
37+
* Supports the follow-modules: <code>Jdk8Module</code>, <code>JavaTimeModule</code>,
38+
* <code>ParameterNamesModule</code> and <code>KotlinModule</code>.
39+
* @return The list of instantiated modules.
40+
*/
41+
@SuppressWarnings("unchecked")
42+
public static List<Module> instantiateAvailableModules() {
43+
List<Module> modules = new ArrayList<>();
44+
try {
45+
Class<? extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class<? extends Module>) ClassUtils
46+
.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null);
47+
com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass);
48+
modules.add(jdk8Module);
49+
}
50+
catch (ClassNotFoundException ex) {
51+
// jackson-datatype-jdk8 not available
52+
}
53+
54+
try {
55+
Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class<? extends Module>) ClassUtils
56+
.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null);
57+
com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
58+
modules.add(javaTimeModule);
59+
}
60+
catch (ClassNotFoundException ex) {
61+
// jackson-datatype-jsr310 not available
62+
}
63+
64+
try {
65+
Class<? extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class<? extends Module>) ClassUtils
66+
.forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null);
67+
com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils
68+
.instantiateClass(parameterNamesModuleClass);
69+
modules.add(parameterNamesModule);
70+
}
71+
catch (ClassNotFoundException ex) {
72+
// jackson-module-parameter-names not available
73+
}
74+
75+
// Kotlin present?
76+
if (KotlinDetector.isKotlinPresent()) {
77+
try {
78+
Class<? extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class<? extends Module>) ClassUtils
79+
.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", null);
80+
Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass);
81+
modules.add(kotlinModule);
82+
}
83+
catch (ClassNotFoundException ex) {
84+
// jackson-module-kotlin not available
85+
}
86+
}
87+
return modules;
88+
}
89+
90+
}

0 commit comments

Comments
 (0)