首页 > 解决方案 > 我的 CustomDeserializer 类在同一类的第二个字段上第二次不起作用

问题描述

我正在使用 Jackson 依赖项,我认为问题在于 jsonParser 被调用了三次以上。但我不确定为什么会这样......我有这个案例:

@Entity 
public class Car implements Serializable {

   @JsonDeserialize(using = CustomDeserialize.class)
   private Window windowOne:

   @JsonDeserialize(using = CustomDeserialize.class)
   private Window windowSecond:
   ....//Getters/Setters


}

CustomDeserializer 类

public class CustomDeserializer extends StdDeserializer<Window> {

  .....  // constructors


@Override
public Window deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
    JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
    String field = jsonParser.nextFieldName();
    String nextField = jsonParser.nextFieldName();
      return new Window("value1", "valu2");
    }
}

调用 objectMapper 的管理器类

 public class Manager {

    private ObjectMapper mapper = new ObjectMapper();


    public void serializeCar(ObjectNode node) {
         // node comes loaded with valid values two windows of a Car.
         // All is OK until here, so this treeToValue enters to CustomDeserializer once only.
         // treeToValue just read the first window ?  because of second window is null and the first window enters on mode debug. 
         Car car = mapper.treeToValue(node, Car.class);
     }

 }

当我调试时,我不知道为什么 treeToValue(objectNode, class) 只调用一次 CustomSerializer 类而第二次不调用它。请问这里有什么问题吗?或者为什么 mapper.treeToValue 使用 CustomDeserializer 忽略第二个字段?先谢谢各位专家了。

更新

我添加了一个存储库作为示例:

https://github.com/NextSoftTis/demo-deserializer

标签: javaspring-bootjacksondeserialization

解决方案


您的解串器工作不正常。

当您到达windowOne时,您正在阅读接下来两个字段的名称 -"windowSecond"并且null(因为我们没有标记) - 而不是JsonNode您已阅读的值。当序列化程序返回时,Jackson 会看到没有更多的令牌,并跳过windowSecond的反序列化,因为没有更多的数据可以使用。

@Override
public Window deserialize(JsonParser jsonParser, DeserializationContext dc) throws IOException, JsonProcessingException {
    JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
    String field = jsonParser.nextFieldName();
    String nextField = jsonParser.nextFieldName();
    return new Window(field + nextField, jsonNode.getNodeType().toString());
}

您可以通过查看示例程序的输出来看到这一点:

{
    "windowOne": {
        "value1": "windowSecondnull",
        "value2": "OBJECT"
    },
    "windowSecond": null
}

(顺便说一下,您的示例存储库不包含您在此处发布的相同代码)。

这些行:

String field = jsonParser.nextFieldName();
String nextField = jsonParser.nextFieldName();

是问题所在,您应该使用JsonNode您已阅读的内容,它会按预期工作:

@Override
public Window deserialize(JsonParser jsonParser, DeserializationContext dc) throws IOException, JsonProcessingException {
    JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
    String value1 = jsonNode.hasNonNull("value1") ? jsonNode.get("value1").asText() : null;
    String value2 = jsonNode.hasNonNull("value2") ? jsonNode.get("value2").asText() : null;
    return new Window(value1, value2);
}

回复:

{
    "windowOne": {
        "value1": "Testing 1",
        "value2": "Testing 2"
    },
    "windowSecond": {
        "value1": "Testing 1 1",
        "value2": "Testing 1 2"
    }
}

深入解释

为了详细说明原始代码中究竟发生了什么,让我们简化一下 JSON 解析器中发生的事情:

我们正在解析的构造JsonNode表示以下 JSON:

{
    "windowOne": {
        "value1": "Testing 1",
        "value2": "Testing 2"
    },
    "windowSecond": {
        "value1": "Testing 1 1",
        "value2": "Testing 1 2"
    }
}

解析器对此进行标记以允许我们使用它。让我们将 this 的标记化状态表示为这个标记列表:

START_OBJECT
FIELD_NAME: "windowOne"
START_OBJECT
FIELD_NAME: "value1"
VALUE: "Testing 1"
FIELD_NAME: "value2"
VALUE: "Testing 2"
END_OBJECT
FIELD_NAME: "windowSecond"
START_OBJECT
FIELD_NAME: "value1"
VALUE: "Testing 1 1"
FIELD_NAME: "value2"
VALUE: "Testing 1 2"
END_OBJECT

杰克逊通过这些代币,试图用它建造一辆汽车。它找到START_OBJECT, 然后FIELD_NAME: "windowOne"它知道它应该被Window反序列化,CustomDeserialize因此它创建 aCustomDeserialize并调用它的deserialize方法。

然后反序列化器调用JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);期望下一个令牌是一个START_OBJECT令牌并解析所有内容直到匹配的END_OBJECT令牌,并将其作为JsonNode.

这将返回JsonNode代表此 JSON 的 a:

{
    "value1": "window 2 value 1",
    "value2": "window 2 value 2"
}

解析器中剩余的标记将是:

FIELD_NAME: "windowSecond"
START_OBJECT
FIELD_NAME: "value1"
VALUE: "Testing 1 1"
FIELD_NAME: "value2"
VALUE: "Testing 1 2"
END_OBJECT
END_OBJECT

然后,您调用String field = jsonParser.nextFieldName();记录为:

获取下一个令牌的方法(就像调用 nextToken 一样)并验证它是否是 JsonToken.FIELD_NAME;如果是,则返回与 getCurrentName() 相同,否则返回 null

即它消耗FIELD_NAME: "windowSecond"和返回"windowSecond"。然后您再次调用它,但由于下一个标记是START_OBJECTthis 返回 null。

我们现在有

field = "windowSecond"
nextField = null
jsonNode.getNodeType().toString() = "OBJECT"

和剩余的令牌:

FIELD_NAME: "value1"
VALUE: "Testing 1 1"
FIELD_NAME: "value2"
VALUE: "Testing 1 2"
END_OBJECT
END_OBJECT

您的反序列化器Window通过传递field + nextField( ="windowSecondnull") 和jsonNode.getNodeType().toString( ="OBJECT") 将其转换为 a ,然后返回,将解析器的控制权传递回 Jackson,Jackson 首先设置Car.value1您的反序列化器返回的窗口,然后继续解析。

这就是它变得有点奇怪的地方。在您的反序列化器返回后,Jackson 期待一个FIELD_NAME令牌,并且由于您消耗了START_OBJECT令牌,因此它得到了一个。但是,它得到FIELD_NAME: "value1"并且因为Car没有任何命名的属性value1 ,并且您已将 Jackson 配置为忽略未知属性,所以它会跳过此字段及其值并继续移动到FIELD_NAME: "value2"导致相同行为的位置。

现在剩余的令牌看起来像这样:

END_OBJECT
END_OBJECT

下一个标记END_OBJECT表明您Car已正确反序列化,因此杰克逊返回。

这里要注意的是解析器仍然有一个剩余的令牌,最后一个END_OBJECT 但由于杰克逊默认忽略剩余的令牌,这不会导致任何错误。

如果您想看到它失败,请删除该行mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

无法识别的字段“value1”(com.example.demodeserializer.Car 类),未标记为可忽略(2 个已知属性:“windowSecond”、“windowOne”])

使用令牌的自定义反序列化器

要编写一个多次调用解析器的自定义反序列化器,我们需要删除该行JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);并自己处理标记。

我们可以这样做:

@Override
public Window deserialize(JsonParser jsonParser, DeserializationContext dc) throws IOException, JsonProcessingException {
    // Assert that the current token is a START_OBJECT token
    if (jsonParser.currentToken() != JsonToken.START_OBJECT) {
        throw dc.wrongTokenException(jsonParser, Window.class, JsonToken.START_OBJECT, "Expected start of Window");
    }

    // Read the next two attributes with value and put them in a map
    // Putting the attributes in a map means we ignore the order of the attributes
    final Map<String, String> attributes = new HashMap<>();
    attributes.put(jsonParser.nextFieldName(), jsonParser.nextTextValue());
    attributes.put(jsonParser.nextFieldName(), jsonParser.nextTextValue());

    // Assert that the next token is an END_OBJECT token
    if (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        throw dc.wrongTokenException(jsonParser, Window.class, JsonToken.END_OBJECT, "Expected end of Window");
    }

    // Create a new window and return it
    return new Window(attributes.get("value1"), attributes.get("value2"));
}

推荐阅读