为什么杰克逊2不认识第一个大写字母,如果前导骆驼大小写单词只有一个字母长?

2022-08-31 15:52:14

我正在使用Spring 4 MVC和Jackson 2进行我的服务。对于其中一个操作,我有一个请求对象,该对象具有一个属性,其中前导骆驼大小写单词的长度仅为一个字母:

private String aLogId;

此类具有适当命名的 getter 和 setter:

public String getALogId() { return aLogId; }
public void setALogId(String aLogId) { this.aLogId = aLogId; }

但是,当我尝试使用相应的JSON属性向此服务发布请求时:

{"aLogId":"This is a log id"}

我收到来自Spring框架的500响应,说该字段无法识别,并且我的控制器类从未被调用:

无法读取 JSON:无法识别的字段“aLogId”(类

但是,当我将“L”更改为小写时,请求将按预期反序列化,并且我的控制器类被命中:

{"alogId":"This is a log id"}

为什么 Jackson 期望“L”是小写的,而它显然是驼峰大小写约定中属性的第二个单词,并且打算使用大写?是因为第一个单词只有一个字母长吗?

请求对象中还有其他属性,其中第一个单词是多个字母,并且那些属性的属性在以防万一时不会遇到相同的不匹配问题。


答案 1

您看到的问题是由于 Jackson 使用 Java Bean 命名约定来找出 Java 类中的 Json 属性。

以下是您看到的具体问题的参考,建议不要将您所在领域的前两个字母中的任何一个大写。如果您使用像IntelliJ或eclipse这样的IDE,并让IDE为您生成设置器,您会注意到发生了相同的“行为”,您最终会得到以下方法:

public void setaLogId(String aLogId) {
    this.aLogId = aLogId;
}

public String getaLogId() {
    return aLogId;
}

因此,当您将“L”更改为小写时,Jackson能够弄清楚您想要映射的字段。

话虽如此,您仍然可以使用“aLogId”字段名称并使Jackson工作,您所要做的就是在其中使用注释。@JsonPropertyaLogId

@JsonProperty("aLogId")
private String aLogId;

以下测试代码将演示这将如何工作:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Test {

    @JsonProperty("aLogId")
    private String aLogId;

    public void setaLogId(String aLogId) {
        this.aLogId = aLogId;
    }

    public String getaLogId() {
        return aLogId;
    }

    public static void main(String[] args) {

        ObjectMapper objectMapper = new ObjectMapper();

        Test test = new Test();

        test.setaLogId("anId");

        try {
            System.out.println("Serialization test: " + objectMapper.writeValueAsString(test));


            String json = "{\"aLogId\":\"anotherId\"}";

            Test anotherTest = objectMapper.readValue(json, Test.class);

            System.out.println("Deserialization test: " +anotherTest.getaLogId());

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

测试的输出为:

Serialization test: {"aLogId":"anId"}

Deserialization test: anotherId


答案 2

@JsonProperty正如当前答案所建议的,缺点是您需要为每个属性重复它,并且它是侵入性的(您需要更改要映射的类)。

更通用的方法是提供自定义属性命名策略

爪哇

public class CustomSnakeCase extends PropertyNamingStrategy.PropertyNamingStrategyBase {
    private static final Pattern REGEX = Pattern.compile("[A-Z]");

    @Override
    public String translate(String input) {
        if (input == null)
            return input; // garbage in, garbage out

        if (!input.isEmpty() && Character.isUpperCase(input.charAt(0)))
            input = input.substring(0, 1).toLowerCase() + input.substring(1);

        return REGEX.matcher(input).replaceAll("_$0").toLowerCase();
    }
}

Kotlin:

class CustomSnakeCase : PropertyNamingStrategy.PropertyNamingStrategyBase() {
    private companion object {
        val REGEX = Regex("[A-Z]")
    }

    override fun translate(input: String?) =
        input?.decapitalize()?.replace(REGEX, "_$0")?.toLowerCase()
}

用法:

new ObjectMapper()
    .setPropertyNamingStrategy(new CustomSnakeCase())
    .enable(MapperFeature.USE_STD_BEAN_NAMING)

注意:我上面提供的实现假设输入是(没有大写的开头)。 需要处理 1 个字符的前缀,例如 consistely。camelCaseUSE_STD_BEAN_NAMINGaField

该实现提供以下映射,您可以根据需要进行调整:

camelCase      snake_case
----------------------------
simple         simple
a              a
sepaRated      sepa_rated
iOException    i_o_exception
xOffset        x_offset
theWWW         the_w_w_w
sepaRated32    sepa_rated32
sepa32Rated    sepa32_rated