My CustomDeserializer class doesn't work second time that is used on second field of a same class

user10713028 :

I'm using Jackson dependencies, I think the issue is when jsonParser is called more three times. but I'm not sure why it is happening that.... I have this case:

@Entity 
public class Car implements Serializable {

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

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


}

CustomDeserializer class

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");
    }
}

Manager class which call 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);
     }

 }

When I debug I don't know why treeToValue(objectNode, class) just calls one time to the CustomSerializer class and second time doesn't call it. Please what is wrong here? or why mapper.treeToValue ignores the second field using CustomDeserializer?. Thanks in advance, experts.

UPDATED

I added a repository as example:

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

Raniz :

Your deserialiser is not working properly.

When you reach windowOne you're reading the names of the next two fields - "windowSecond" and null (since we're out of tokens) - instead of the values of the JsonNode you've read. When the serializer returns, Jackson then sees that there are no more tokens and skips deserialisation of windowSecond because there is no more data to consume.

@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());
}

You can see this by looking at the output of your example program:

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

(your sample repo does not contain the same code you posted here by the way).

The lines:

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

is the issue, you should use the JsonNode you've read instead and it'll work as expected:

@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);
}

Response:

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

In-depth Explanation

To elaborate on what exactly happens in the original code, let's take a simplified look on what happens in the JSON parser:

The constructed JsonNode that we're parsing represents the following JSON:

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

The parser tokenizes this to allow us to work with it. Let's represent the tokenized state of this as this list of tokens:

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

Jackson the goes through these tokens, trying to construct a car out of it. It finds START_OBJECT, then FIELD_NAME: "windowOne" which it knows should be a Window deserialised by CustomDeserialize so it creates a CustomDeserialize and calls it's deserialize method.

The deserialiser then calls JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser); which expects the next token to be a START_OBJECT token and parses everything up until the matching END_OBJECT token, returning it as a JsonNode.

This will return a JsonNode that represents this JSON:

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

And the remaining tokens in the parser will be:

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

You then call String field = jsonParser.nextFieldName(); which is documented as:

Method that fetches next token (as if calling nextToken) and verifies whether it is JsonToken.FIELD_NAME; if it is, returns same as getCurrentName(), otherwise null

I.e. it consumes FIELD_NAME: "windowSecond" and returns "windowSecond". You then call it again, but since the next token is START_OBJECT this returns null.

We now have

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

and the remaining tokens:

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

Your deserialiser turns this into a Window by passing field + nextField (="windowSecondnull") and jsonNode.getNodeType().toString (="OBJECT") and then returns, passing control of the parser back to Jackson which first sets Car.value1 to the window your deserialiser returned, and then continues parsing.

Here's where it gets a little weird. After your deserializer returns, Jackson is expecting a FIELD_NAME token and since you consumed the START_OBJECT token it gets one. However, it gets FIELD_NAME: "value1" and since Car doesn't have any attribute named value1 and you have configured Jackson to ignore unknown properties it skips this field and it's value and moves on to FIELD_NAME: "value2" which causes the same behaviour.

Now the remaining tokens looks like this:

END_OBJECT
END_OBJECT

The next token is END_OBJECT which signals that your Car has been properly deserialised so Jackson returns.

The thing to note here is that the parser still has one remaining token, the last END_OBJECT but since Jackson ignores remaining tokens by default that doesn't cause any errors.

If you want to see it fail, remove the line mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);:

Unrecognized field "value1" (class com.example.demodeserializer.Car), not marked as ignorable (2 known properties: "windowSecond", "windowOne"])

Custom deserialiser that consumes tokens

To write a custom deserialiser that calls the parser multiple times we need to remove the line JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser); and handle the tokens ourselves instead.

We can do that like this:

@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"));
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=99527&siteId=1