`NullNode#asString` and `NullNode#asString(String)` discrepency with Jackson 2
Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
There is a discrepency between the asString behavior of NullNode on Jackson 3 and Jackson 2.
Version Information
3.0.1 and 2.20.1
Reproduction
If we take:
System.out.println(NullNode.getInstance().asText());
System.out.println(NullNode.getInstance().asText("default"));
Expected behavior
With Jackson 2 it prints:
null
default
With Jackson 3 it prints:
i.e. both methods return an empty String on Jackson 3. However, with Jackson 2
Additional context
I did find #5034, but I could not understand the reasoning for the change of the asString(String). For asString() you can argue that maybe an empty string is fine, but why empty string for asString(String)? I also did see #5287 which makes stringValue(String) return the default value for null nodes.
We have a lot of parsing code that uses JsonNode and we use a lot object.path("someProperty").asText("default") to get a value for it. The pattern for this now should be object.path("someProperty").stringValue("default"), but this is something that did not exist in Jackson 2.
First things first: behavior of Jackson 3 is not really expected to be same as that of Jackson 2.x: a big reason for major version upgrade is the ability to fix things that cannot be fixed within minor updates (behavior changes).
So 3.0 has changed behavior of JsonNode accessor methods, making them more consistent and hopefully overall better.
Second thing is that the intent between "xxxValue()" and "asXxx()" methods, in 3.x, is clarified to be:
- "xxxValue()": access value as-is with no type coercion: if type does not match (call "intValue()" on JSON String node f.ex), throw exception
- "asXXX()": if matching type, return as-is; if not but can coerce (JSON String that represents number, ok to access as number); only throw exception if no coercion avaialble (can't get String from Array, f.ex)
In addition there are "optional"/defaulting accessors to provide value instead of exception.
Third: "null"-handling is tricky: there is JSON null value, distinct from JSON String. But in Java, null is valid for referential types. So shoud JSON null be return as matching value or not?
Fourth: most Java developers seem to prefer avoiding nulls as much as possible, so new (3.x) access tries to reduce use of nulls.
Having said all of that, the key question is Jackson 3 handling of JSON null via String accessors.
I think JsonNode.stringValue() does return Java null for JSON null -- this was a late change right before 3.0.0 GA.
But I think like you say, "asString()" accessors consider JSON null to require coercion, and thereby return "default" / Optional.empty() value, not Java null. This is mostly wrt Fourth point above.
With all of that, what change in 3.0 do you find most problematic? Or does above explanation clarify intent and meaning of changes.
We have a lot of parsing code that uses JsonNode and we use a lot object.path("someProperty").asText("default") to get a value for it. The pattern for this now should be object.path("someProperty").stringValue("default"), but this is something that did not exist in Jackson 2.
If large code base change is your concern, there is community-provided OpenRewrite receipe that can be used @filiphr
Thanks a lot @cowtowncoder for the detailed post.
First things first: behavior of Jackson 3 is not really expected to be same as that of Jackson 2.x: a big reason for major version upgrade is the ability to fix things that cannot be fixed within minor updates (behavior changes).
I fully agree with you here. The new approach with throwing exceptions when coercion cannot happen is more correct and better for coding since it avoids accidental use of wrong methods.
The change that I personally find the most problematic is the asXXX(XXX), e.g. asString(String). For null the default value is not returned. This makes using the JsonNode more tricky.
JsonNode node = ...;
String type = node.path("type").asString("test");
The code above with Jackson 3 would give test when using NullNode, MissingNode. However, with Jackson 3 it returns empty string when using NullNode and test when using MissingNode.
So in order to achieve the same thing in Jackson 3 someone needs to write something like:
JsonNode node = ...;
JsonNode typeNode = node.path("type");
String type = typeNode.isNull() : "test" : typeNode.asString("test");
Perhaps the alternative is to do something like:
String type = node.path("type").stringValue("test");
However, for some other types, where you want coercion to happen e.g. Boolean you cannot do booleanValue(true). e.g.
boolean isTransient = outParameter.path("transient").asBoolean(true);
This would return:
true- For MissingNodefalse- For StringNode withfalsetextfalse- For NullNode
If we use booleanValue(true) then we would have
true- For MissingNodetrue- For StringNode withfalsetexttrue- For NullNode
Thanks @JooHyukKim for sharing the OpenRewrite recipes. My concern is not the changes of the methods names or packages, that's the easy part. My main concern is the change in the behaviour of certain methods.