spring-projects / spring-ai

An Application Framework for AI Engineering
https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/index.html
Apache License 2.0
2.35k stars 540 forks source link

Message Serialization: Enhance deserialization #688

Closed AZCodingAccount closed 2 weeks ago

AZCodingAccount commented 1 month ago

Background

Hey there, while developing with Spring Boot, I encountered issues with Jackson deserialization when storing historical messages into the Redis client. After troubleshooting using the control variable method, I found that there is room for improvement in the classes under the Message package. I made two code modifications to accommodate the new version of Jackson's deserialization process.

Solution

  1. I added no-argument constructors to UserMessage, SystemMessage, FunctionMessage, and AssistMessage, and modified AbstractMessage to include a single-argument constructor. This resolves the following serialization error:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.ai.chat.messages.SystemMessage` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
  1. I renamed textContent and mediaData in AbstractMessage to content and media to align with the methods exposed by the message. This change can resolve the following error:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Problem deserializing 'setterless' property ("media"): no way to handle typed deser with setterless yet

PS: Additional Information

Version Information: Spring Boot 3.2.4, spring-boot-starter-data-redis 3.2.4, SpringAI 1.0.0-SNAPSHOT

RedisTemplateConfig.java:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer<Object> serializer = redisSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    }
}
markpollack commented 3 weeks ago

Can we use the approach that adds Jackson annotations since creating an instance of a message without content in code does not make sense. For example.

class MyClass {
    private final int id;
    private final String name;

    @JsonCreator
    public MyClass(@JsonProperty("id") int id, @JsonProperty("name") String name) {
        this.id = id;
        this.name = name;
    }

....
AZCodingAccount commented 2 weeks ago

thx