防止 Jackson ObjectMapper 自动以 String() 格式格式化复合映射键

Prevent Jackson ObjectMapper automatically toString()-formatting composite map keys

我正在使用 com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString() 从 Java POJO 生成 JSON。

我的一个对象 - 让我们称之为 ResultSheet - 包含一个 属性 Map<ResultId,Integer> resultsResultId 表示复合 PK(两个 FK)。

似乎 ObjectMapper 在遇到 ResultId 时意识到这不能直接在 JSON 中表示,因为 JSON 映射键必须是字符串。因此它输出的是 toString() of ResultId。 (在这种特殊情况下,这会产生一个 "42:43" 形式的字符串,但这只是因为这恰好是 toString() 当前的实现方式。)

我怎样才能阻止它这样做?当前的行为隐藏了这样一个事实,即被编组的结构不能正确表示为 JSON 结构。它还将当前 toString() 实现泄漏到我的网络服务的 API 中。

我宁愿抛出异常告诉我用作映射键的值何时不是 String - 或者至少,当转换为 String 并非易事时. (例如,我不太担心 Long 键被 toString() 编辑。)

我查看了 SerializationFeature 设置,但其中 none 似乎解决了这个问题。 https://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/SerializationFeature.html

有什么办法可以避免这种情况吗?能否以一般方式完成,即不仅针对这个特定的 class,而且在未来可能出现的任何情况下,作为新的 classes,其中一些可能用作映射键,是已添加?

请注意,在这种情况下,使 toString() 抛出异常是不可接受的。

要解决此问题,您可以实施并注册自己的 com.fasterxml.jackson.databind.ser.BeanSerializerModifier class。并覆盖 modifyKeySerializer 方法。实现总是可以 return 一个抛出异常的序列化程序。请参阅以下示例:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

public class JsonKeySerializerApp {
    public static void main(String[] args) throws IOException {
        SimpleModule keyExceptionSerializerModule = new SimpleModule();
        keyExceptionSerializerModule.setSerializerModifier(new BeanSerializerModifier() {
            @Override
            public JsonSerializer<?> modifyKeySerializer(SerializationConfig config, JavaType valueType, BeanDescription beanDesc, JsonSerializer<?> serializer) {
                // specify classes you want to allow to be serialized as keys
                if (valueType.getRawClass().getPackage().getName().startsWith("java.")) {
                    return serializer;
                }
                return new JsonSerializer<Object>() {
                    @Override
                    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) {
                        throw new UnsupportedOperationException("You can not serialize POJO as keys!");
                    }
                };
            }
        });
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(keyExceptionSerializerModule);

        Map<ResultId, Long> map = Collections.singletonMap(new ResultId(1, 2), 1L);

        mapper.writeValue(System.out, map);
    }
}

@AllArgsConstructor
@Data
class ResultId {

    private int id1;
    private int id2;
}

以上代码打印:

{Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: You can not serialize POJO as keys! (through reference chain: java.util.Collections$SingletonMap["ResultId(id1=1, id2=2)"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
    at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:316)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:725)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:643)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:33)