Reputation:
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
Upvotes: 3
Views: 730
Reputation: 11113
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"));
}
Upvotes: 4