我的 CustomDeserializer 类在同一类的第二个字段上第二次不起作用

Posted

技术标签:

【中文标题】我的 CustomDeserializer 类在同一类的第二个字段上第二次不起作用【英文标题】:My CustomDeserializer class doesn't work second time that is used on second field of a same class 【发布时间】:2019-05-08 18:09:46 【问题描述】:

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

@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的Manager类

 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

【问题讨论】:

你能在 github 上分享这个 bug 的 poc 吗? 我想,没必要。因为你有类作为例子,但如果你真的需要它,我会创建它。 我对我的一个应用程序使用相同的配置,但我无法发现你的 wath 有问题,如果你可以创建一个 github 存储库,其中包含需要重现的 juste wath,我会尝试为你修好它 完成github.com/NextSoftTis/demo-deserializer 顺便说一句,在示例存储库上做得很好。有一个可运行的示例可以让回答问题很多更容易! 【参考方案1】:

您的反序列化器工作不正常。

当您到达 windowOne 时,您正在读取接下来两个字段的名称 - "windowSecond"null(因为我们没有令牌) - 而不是 @ 的值987654325@您已阅读。当序列化程序返回时,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"
    

解析器tokenizes 这允许我们使用它。让我们将 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",它知道应该是CustomDeserialize反序列化的Window,因此它创建了一个CustomDeserialize并调用它的deserialize方法。

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

这将返回代表此 JSON 的 JsonNode


    "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();,记录为:

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

即它消耗FIELD_NAME: "windowSecond" 并返回"windowSecond"。然后您再次调用它,但由于下一个令牌是 START_OBJECT,因此返回 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

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

这里有点奇怪。在您的反序列化器返回后,Jackson 期待一个 FIELD_NAME 令牌,并且由于您使用了 START_OBJECT 令牌,它得到一个。但是,它得到 FIELD_NAME: "value1" 并且由于 Car 没有任何名为 value1 的属性 并且 您已将 Jackson 配置为忽略未知属性,它会跳过该字段及其值并转到 @ 987654365@ 导致相同的行为。

现在剩余的令牌如下所示:

END_OBJECT
END_OBJECT

下一个标记是 END_OBJECT,这表明您的 Car 已正确反序列化,因此 Jackson 返回。

这里要注意的是解析器还有一个剩余的标记,最后一个END_OBJECTbut since Jackson ignores remaining tokens by default不会导致任何错误。

如果您想看到它失败,请删除 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"));

【讨论】:

看起来不错,我有一个问题,当你在反序列化方法中多次调用 jsonParser 时,这是我认为的主要问题,你知道为什么我不能多次调用 jsonParser 来做一些逻辑..? 是的,我也有@nextsoft 说的同样的问题,多次调用jsonParser 不正确吗?或者为什么有些事情会通过...... 我无法在评论中添加解释,因此更新了答案

以上是关于我的 CustomDeserializer 类在同一类的第二个字段上第二次不起作用的主要内容,如果未能解决你的问题,请参考以下文章

java 子类与父类

Java修饰类

为啥我的电影类在创建电影实例时遇到问题?

AWK Mac OSX如何在同一行打印数组键和数组值

重写和重载

如何从我的主类在画布上绘制一个矩形?