-
Notifications
You must be signed in to change notification settings - Fork 3
JSTEP 3
(Back to JSTEP page)
Related change issues: [fill me in]
Tatu Saloranta (@cowtowncoder)
- 2024-12-21: Further refinement
- 2024-12-19: Minor additions
- 2024-06-02: Remove details of
JsonNodeFeature - 2022-01-16: Add links to JSTEP-7 (new
JsonNodeFeatureas one ofDataTypeFeatures) - 2022-01-04: Add
ALLOW_ARRAY_MERGE,OBJECT_MERGEJsonNodeFeatures. - 2019-01-26: first skeletal revision
Although "Tree Model" -- operating on json content via JsonNode-based object model which represents content exactly as-is, without transformations -- has been around since Jackson 1.0, it has not been worked on as extensively as full databinding to/from POJOs. But it is extensively used and valued by users due to its flexibility. Over time some of original design choices have proven problematic (for example methods not being able to throw JsonProcessingException for invalid coercions; or returning of JsonNode for methods in contaner nodes, preventing chaining of many calls), and in a way that can not be changed without breaking backwards compatibility.
With 3.0 we have a perfect opportunity to further improve JsonNode API, as well as fix problems in return type declarations. We can also benefit from other changes, in particular JSTEP-4 which will make it possible for all methods to throw properly typed exceptions.
As of Jackson 2.x, only some of DeserializationFeatures (and few if any SerializationFeatures) affect handling of JsonNode. This is due to most of them being POJO-centric, as JsonNode is meant to be faithful representation of JSON that was read, or is to be written out, with few changes.
But users do have legitimate need/desire to make some changes. It's just that these changes are not necessarily aligned with changes to POJO handling. For this reason, Jackson 2.14 added JsonNodeFeature for configurability.
More details in JSTEP-7.
With 3.0, JacksonException becomes unchecked (see JSTEP-4)
This is relevant for JsonNode, too, since we can start throwing formal Jackson exceptions, without having to declare them separately for all (or just some) accessors.
This is relevant for a few entries here:
-
JsonNode.toString()can pass any exceptions from real serialization as-is, with no additional wrapping
However: I think it also makes sense to introduce at least one new Jackson exception type, like:
-
ValueCoercionException(similar to new Streaming API exception,InputCoercionException)
which can then be used by methods that attempt coercion (like JsonNode.asIntValue()), but fail due to incompatible types (or perhaps parsing error, from String to number).
This exception type should retain and expose information like:
- Source token type (
JsonToken.VALUE_STRING) - Target shape (as token, like
JsonToken.VALUE_NUMBER_INT) - If available, target Java type (
java.lang.Integer#typeforint, for example) - We do not necessarily have information on path, but possibly exception catch/rethrow could re-create this
Rename methods with "text" to use "string", similarly types ("TextNode" -> "StringNode"): deprecate old methods in cases where significant usage exists (textValue(), asText())
-
databind#2145
-
Optional<JsonNode> optional(String): forObjectNode, counterpart toget()/path()(Optional.empty()by non-ObjectNode, or missing entry) -
Optional<JsonNode> optional(int): forArryNode, counterpart toget()/path()(Optional.empty()by non-ArrayNode, or index out-of-bounds)- Alternative names considered:
opt(),getOpt()',getOptional()` (shorter, longer)? -
getOpt()might be best wrt IDE auto-completion, JavaDocs (gets grouped close toget()variants)
- Alternative names considered:
-
To support Java 8+ Stream traversal, we propose additions of following methods in JsonNode, with actual implementations in ArrayNode and/or ObjectNode (and for other placeholder default):
-
databind#4863
-
Stream<JsonNode> valueStream()forArrayNodeelements andObjectNodevalues; emptyStreamfor other node types -
Stream<Map.Entry<String, JsonNode>> propertyStream()forObjectNodeentries; emptyStreamfor other node types -
forEachEntry(BiConsumer<? super String, ? super JsonNode>)for iterating overObjectNodeproperties; NOP for other node types
-
In future we could also consider "parallel" variants for streams, and "spliterator".
- For
ContainerNode(ObjectNode,ArrayNode):-
removeIf(Predicate<? super JsonNode>): general purpose -
removeNulls()(short-cut forremoveIf(JsonNode::isNullNode)) -
removeEmpty()(short-cut forremoveIf(JsonNode::isEmpty)) -
clear()(remove all entries): named similar toCollectionoperation- NOTE:
removeAll()already exists, need an alias?
- NOTE:
-
We could consider alternative names:
- Longer names:
removeNullValues(),removeEmptyValues()?
-
databind#4868
-
ArrayNode JsonNode.asArray()to mean "ifArrayNodereturn as-is -- if not, wrap as 1-elementArrayNode" (or emptyArrayNodefor `MissingNode)
-
-
databind#4867:
-
JsonNode.asOptional()which would give non-emptyOptional<JsonNode>for all nodes EXCEPT forMissingNodefor whichOptional.empty()would be returned
-
As of 2.x, there are a few accessors for type like, say, int:
-
intValue(): returnintif (and only if!) we got numeric integer node (JsonToken.VALUE_NUMBER_INT) -- but won't throw exception, returns default value (0) otherwise -
asInt(): same asasInt(0) -
asInt(int defaultValue): returnintvalue fromJsonToken.VALUE_NUMBER_INT(if within value range) OR if coercible (fromdoubleorString); otherwise returndefaultValue
But none actually throws exception: partly due to historical reasons (was not done initially), and also since methods do not expose JacksonException (or IOException). So, due to backwards compatibility.
So there are couple of things to improve:
- Should allow throwing exceptions, for case where no default is specified. We can now do this more easily as we throw unchecked exception -- meaning accessors are still safe with Java 8 streaming
- Should allow use of Java 8
Optional, as that is useful forstream()operations
This would leave to bigger set of methods, once again for int:
-
intValue()as before, return value ifJsonToken.VALUE_NUMBER_INT, within Javaintrange -- but if not, throw exception -
intValue(int defaultValue)asintValue()except returnsdefaultValueinstead of exception -
optIntValue()similar tointValue()but returnsOptionalInt, either present (as perintValue()) or absent (instead of exception) -
asInt()(orasIntValue()?) similar tointValue()but allows coercion fromdoubleandString(and possible with config, fromtrue/false). But if not, throw exception -
asInt(int defaultValue)likeasInt()but instead of exception, returndefaultValue()for case of no valid value -
asOptInt()(maybe?): similar toasInt()but instead of exception, return "absent" value
Now: set of operations may be smaller for other more esoteric types.
-
JsonNode.toString()will guarantee valid JSON output (assuming default vanillaObjectMappersettings)-
JsonNode.isEmpty()works as alias for idiomsize() == 0
-
(note: these were added via databind#2237)
-
required(String / int)for basically "get, throw exception if no node with name/index) -
requiredAt(JsonPointer): same as above, but withJsonPointer -
require()-- returnsJsonNode(or subtype, co-variant) unlessMissingNode; ifMissingNode, throw exception (unchecked, Jackson-specific) -
requireNonNull()-- same asrequire(), but fail both forMissingNodeandNullNode