如何指示 Jackson ObjectMapper 不将数字字段值转换为字符串 属性?

How to instruct Jackson ObjectMapper to not convert number field value into a String property?

我有以下 JSON 示例:

{
    "channel": "VTEX",
    "data": "{}",
    "refId": 143433.344,
    "description": "teste",
    "tags": ["tag1", "tag2"]
}

应该映射到以下 class:

public class AddConfigInput {
    public String channel;
    public String data;
    public String refId;
    public String description;
    public String[] tags;

    public AddConfigInput() {
    }
}

使用如下代码:

ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS);
String json = STRING_CONTAINING_THE_PREVIOUS_INFORMED_JSON;
AddConfigInput obj = mapper.readValue(json, AddConfigInput.class);
System.out.println(mapper.writeValueAsString(obj));

作为输出产生:

{"channel":"VTEX","data":"{}","refId":"143433.344","description":"teste","tags":["tag1","tag2"]}

请注意字段 refIdString 类型,我想避免这种从数字到字符串属性的自动转换.相反,我想 Jackson 抛出一个关于类型不匹配的错误。我该怎么做?

检查它是否适合您。

我为属性 refId 添加了一个自定义反序列化器,我正在检查类型,以防数据类型不匹配并抛出异常。

用于强制数据类型检查的自定义解串器。

KeepStringDeserializer.java

package oct2020.json;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

public class KeepStringDeserializer extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser jsonParser,
            DeserializationContext deserializationContext) throws IOException {
        if (jsonParser.getCurrentToken() != JsonToken.VALUE_STRING) {
            throw deserializationContext.wrongTokenException(jsonParser,
                    String.class, JsonToken.VALUE_STRING,
                    "Expected value is string but other datatype found.");
        }
        return jsonParser.getValueAsString();
    }
}

AddConfigInput.java

对于使用自定义解串器的 refId 属性。

package oct2020.json;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

public class AddConfigInput {
    public String channel;
    public String data;
    @JsonDeserialize(using = KeepStringDeserializer.class)
    public String refId;
    public String description;
    public String[] tags;

    public AddConfigInput() {
    }
}

TestClient.java

package oct2020.json;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestClient {
    public static void main(String[] args) throws JsonMappingException,
            JsonProcessingException {
        String json = "{\n    \"channel\": \"VTEX\",\n    \"data\": \"{}\",\n    \"refId\": 143433.344,\n    \"description\": \"teste\",\n    \"tags\": [\"tag1\", \"tag2\"]\n}\"";
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS);
        AddConfigInput obj = mapper.readValue(json, AddConfigInput.class);
        System.out.println(mapper.writeValueAsString(obj));
    }
}

输出:

案例 1:数据不匹配

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (VALUE_NUMBER_FLOAT), Expected value is string but other datatype found.
 at [Source: (String)"{
    "channel": "VTEX",
    "data": "{}",
    "refId": 143433.344,
    "description": "teste",
    "tags": ["tag1", "tag2"]
}""; line: 4, column: 14] (through reference chain: oct2020.json.AddConfigInput["refId"])
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)

案例2:正确数据

输入:

String json = "{\n    \"channel\": \"VTEX\",\n    \"data\": \"{}\",\n    \"refId\": \"143433.344\",\n    \"description\": \"teste\",\n    \"tags\": [\"tag1\", \"tag2\"]\n}\"";

输出:

{"channel":"VTEX","data":"{}","refId":"143433.344","description":"teste","tags":["tag1","tag2"]}

似乎mapper.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS);适用于相反的情况,即在将String值反序列化为数字字段时解析失败。

refId 字段提供自定义反序列化程序似乎可以解决此问题。

public class AddConfigInput {
    public String channel;
    public String data;

    //@JsonDeserialize(using = ForceStringDeserializer.class)
    public String refId;
    public String description;
    public String[] tags;

    public AddConfigInput() {
    }
}
public class ForceStringDeserializer extends JsonDeserializer<String> {

    @Override
    public String deserialize(
            JsonParser jsonParser, DeserializationContext deserializationContext) 
            throws IOException 
    {
        if (jsonParser.getCurrentToken() != JsonToken.VALUE_STRING) {
            deserializationContext.reportWrongTokenException(
                    String.class, JsonToken.VALUE_STRING, 
                    "Attempted to parse token %s to string",
                    jsonParser.getCurrentToken());
        }
        return jsonParser.getValueAsString();
    }
}

更新
此自定义反序列化器可以在 ObjectMapper 中注册并覆盖默认行为:

public class ForcedStringParserModule extends SimpleModule {

    private static final long serialVersionUID = 1L;

    public ForcedStringParserModule() {
        this.setDeserializerModifier(new BeanDeserializerModifier() {

            @Override
            public JsonDeserializer<?> modifyDeserializer(
                    DeserializationConfig config, BeanDescription beanDesc,
                    JsonDeserializer<?> deserializer) 
            {
                if (String.class.isAssignableFrom(beanDesc.getBeanClass())) {
                    return new ForceStringDeserializer();
                }
                return deserializer;
            }
        });
    }
}

那么这个模块可以注册ObjectMapper:

mapper.registerModule(new ForcedStringParserModule ());

稍微修改输入 JSON 后(对必须为字符串的 data 字段使用布尔值),抛出以下异常:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: 
Unexpected token (VALUE_FALSE), expected VALUE_STRING: 
Attempted to parse token VALUE_FALSE to string
 at [Source: (String)"{
    "channel": "VTEX",
    "data": false,
    "refId": "143433.344",
    "description": "teste",
    "tags": ["tag1", "tag2"]
}"; line: 3, column: 13]