Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.1.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-json-parser</artifactId>
<packaging>jar</packaging>
<name>Spring AI JsonParser Auto Configuration</name>
<description>Spring AI JsonParser Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>

<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>

<dependencies>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
<version>${project.parent.version}</version>
</dependency>

<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>

<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.util.json.autoconfigure;

import com.fasterxml.jackson.databind.DeserializationFeature;
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 org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.util.json.JsonParser;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
* Auto-configuration for JsonParser and ModelOptionsUtils ObjectMapper.
* <p>
* Provides customizable ObjectMappers for JSON parsing operations in tool calling,
* structured output, and model options handling. Users can override these beans to
* customize Jackson behavior.
*
* @author Daniel Albuquerque
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@EnableConfigurationProperties(JsonParserProperties.class)
public class JsonParserObjectMapperAutoConfiguration {

/**
* Creates a configured ObjectMapper for JsonParser operations.
* <p>
* This ObjectMapper is configured with:
* <ul>
* <li>Lenient deserialization (doesn't fail on unknown properties)</li>
* <li>Proper handling of empty beans during serialization</li>
* <li>Standard Jackson modules (Java 8, JSR-310, ParameterNames, Kotlin)</li>
* <li>Optional features from JsonParserProperties</li>
* </ul>
*
* To customize, provide your own bean: <pre>{@code
* &#64;Bean
* public ObjectMapper jsonParserObjectMapper() {
* return JsonMapper.builder()
* .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
* .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
* .build();
* }
* }</pre>
* @param properties the JsonParser configuration properties
* @return the configured ObjectMapper
*/
@Bean(name = "jsonParserObjectMapper", defaultCandidate = false)
@ConditionalOnMissingBean(name = "jsonParserObjectMapper")
public ObjectMapper jsonParserObjectMapper(JsonParserProperties properties) {
JsonMapper.Builder builder = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.addModules(JacksonUtils.instantiateAvailableModules());

// Apply properties
if (properties.isAllowUnescapedControlChars()) {
builder.enable(com.fasterxml.jackson.core.json.JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS);
}

if (!properties.isWriteDatesAsTimestamps()) {
builder.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

return builder.build();
}

/**
* Creates a configured ObjectMapper for ModelOptionsUtils operations.
* <p>
* This ObjectMapper is configured with:
* <ul>
* <li>Lenient deserialization (doesn't fail on unknown properties)</li>
* <li>Proper handling of empty beans during serialization</li>
* <li>Standard Jackson modules (Java 8, JSR-310, ParameterNames, Kotlin)</li>
* <li>Empty string to null coercion for objects and enums</li>
* <li>Optional features from JsonParserProperties</li>
* </ul>
*
* To customize, provide your own bean: <pre>{@code
* &#64;Bean
* public ObjectMapper modelOptionsObjectMapper() {
* ObjectMapper mapper = JsonMapper.builder()
* .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
* .addModules(JacksonUtils.instantiateAvailableModules())
* .build()
* .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
*
* mapper.coercionConfigFor(Enum.class)
* .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
*
* return mapper;
* }
* }</pre>
* @param properties the configuration properties
* @return the configured ObjectMapper
*/
@Bean(name = "modelOptionsObjectMapper", defaultCandidate = false)
@ConditionalOnMissingBean(name = "modelOptionsObjectMapper")
public ObjectMapper modelOptionsObjectMapper(JsonParserProperties properties) {
JsonMapper.Builder builder = JsonMapper.builder();

// Apply base configuration
if (properties.isFailOnUnknownProperties()) {
builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
else {
builder.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

if (properties.isFailOnEmptyBeans()) {
builder.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
else {
builder.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}

builder.addModules(JacksonUtils.instantiateAvailableModules());

ObjectMapper mapper = builder.build();

// Configure empty string handling
if (properties.isAcceptEmptyStringAsNull()) {
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}

// Configure enum coercion (critical for API compatibility)
if (properties.isCoerceEmptyEnumStrings()) {
mapper.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
}

return mapper;
}

/**
* Creates a JsonParser bean configured with the custom ObjectMapper. This also sets
* the static configured mapper for backward compatibility with code using static
* methods.
* @param objectMapper the configured ObjectMapper
* @return the JsonParser instance
*/
@Bean
@ConditionalOnMissingBean
public JsonParser jsonParser(@Qualifier("jsonParserObjectMapper") ObjectMapper objectMapper) {
// Set the static mapper for backward compatibility
JsonParser.setConfiguredObjectMapper(objectMapper);

// Create bean instance
return new JsonParser(objectMapper);
}

/**
* Initializes ModelOptionsUtils with the Spring-managed ObjectMapper. This setter
* allows ModelOptionsUtils static methods to use the Spring-configured mapper while
* maintaining backward compatibility.
* @param objectMapper the configured ObjectMapper for model options
*/
@Bean
@ConditionalOnMissingBean(name = "modelOptionsUtilsInitializer")
public Object modelOptionsUtilsInitializer(@Qualifier("modelOptionsObjectMapper") ObjectMapper objectMapper) {
// Set the static mapper for backward compatibility
ModelOptionsUtils.setConfiguredObjectMapper(objectMapper);

// Return a marker object to satisfy bean contract
return new Object();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.util.json.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Configuration properties for JsonParser and ModelOptionsUtils ObjectMapper.
*
* @author Daniel Albuquerque
*/
@ConfigurationProperties(prefix = JsonParserProperties.CONFIG_PREFIX)
public class JsonParserProperties {

public static final String CONFIG_PREFIX = "spring.ai.json";

/**
* Allow unescaped control characters (like \n) in JSON strings. Useful when LLMs
* generate JSON with literal newlines.
*/
private boolean allowUnescapedControlChars = false;

/**
* Write dates as ISO-8601 strings instead of timestamp arrays. When false (default),
* dates are written as strings like "2025-07-03".
*/
private boolean writeDatesAsTimestamps = false;

/**
* Accept empty strings as null objects during deserialization. Used by
* ModelOptionsUtils for handling API responses.
*/
private boolean acceptEmptyStringAsNull = true;

/**
* Coerce empty strings to null for enum types. Critical for handling API responses
* with empty finish_reason values.
*/
private boolean coerceEmptyEnumStrings = true;

/**
* Fail on unknown properties during deserialization. When false (default), unknown
* properties are ignored.
*/
private boolean failOnUnknownProperties = false;

/**
* Fail on empty beans during serialization. When false (default), empty beans are
* serialized as empty objects.
*/
private boolean failOnEmptyBeans = false;

public boolean isAllowUnescapedControlChars() {
return this.allowUnescapedControlChars;
}

public void setAllowUnescapedControlChars(boolean allowUnescapedControlChars) {
this.allowUnescapedControlChars = allowUnescapedControlChars;
}

public boolean isWriteDatesAsTimestamps() {
return this.writeDatesAsTimestamps;
}

public void setWriteDatesAsTimestamps(boolean writeDatesAsTimestamps) {
this.writeDatesAsTimestamps = writeDatesAsTimestamps;
}

public boolean isAcceptEmptyStringAsNull() {
return this.acceptEmptyStringAsNull;
}

public void setAcceptEmptyStringAsNull(boolean acceptEmptyStringAsNull) {
this.acceptEmptyStringAsNull = acceptEmptyStringAsNull;
}

public boolean isCoerceEmptyEnumStrings() {
return this.coerceEmptyEnumStrings;
}

public void setCoerceEmptyEnumStrings(boolean coerceEmptyEnumStrings) {
this.coerceEmptyEnumStrings = coerceEmptyEnumStrings;
}

public boolean isFailOnUnknownProperties() {
return this.failOnUnknownProperties;
}

public void setFailOnUnknownProperties(boolean failOnUnknownProperties) {
this.failOnUnknownProperties = failOnUnknownProperties;
}

public boolean isFailOnEmptyBeans() {
return this.failOnEmptyBeans;
}

public void setFailOnEmptyBeans(boolean failOnEmptyBeans) {
this.failOnEmptyBeans = failOnEmptyBeans;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.springframework.ai.util.json.autoconfigure.JsonParserObjectMapperAutoConfiguration
Loading