Skip to content

JSTEP 3

Tatu Saloranta edited this page Feb 8, 2025 · 55 revisions

(Back to JSTEP page)


JSTEP-3: JsonNode improvements (some for Jackson 3.0, some in 2.x)

Author

Tatu Saloranta (@cowtowncoder)

Version history

  • 2025-02-07: Create last issue for work covered here
  • 2024-06-02: Remove details of JsonNodeFeature
  • 2022-01-16: Add links to JSTEP-7 (new JsonNodeFeature as one of DataTypeFeatures)
  • 2022-01-04: Add ALLOW_ARRAY_MERGE, OBJECT_MERGE JsonNodeFeatures.
  • 2019-01-26: first skeletal revision

Status

In-Progress (but mostly complete), as of 07-Feb-2025

Background

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.

Changes: Configurability

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.

Changes: Exception reporting

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#type for int, for example)
  • We do not necessarily have information on path, but possibly exception catch/rethrow could re-create this

Changes: Renaming (COMPLETE, 3.0)

"Text" -> "String" (COMPLETE, 3.0)

Rename JsonNode.elements() as JsonNode.values() (COMPLETE, 3.0)

  • databind#4869
    • NOTE: left elements() method in ArrayNode as alias (since Arrays have elements, unlike Objects)

Changes: Additional accessors (COMPLETE, 2.19)

Additional Optional-producing getter(s) (COMPLETE, 2.19)

  • databind#2145
    • Optional<JsonNode> optional(String): for ObjectNode, counterpart to get() / path() (Optional.empty() by non-ObjectNode, or missing entry)
    • Optional<JsonNode> optional(int): for ArryNode, counterpart to get() / 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 to get() variants)

Changes: Additional traversal (COMPLETE, 2.19)

Stream access (COMPLETE, 2.19)

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 (COMPLETE, 2.19)
    • Stream<JsonNode> valueStream() for ArrayNode elements and ObjectNode values; empty Stream for other node types
    • Stream<Map.Entry<String, JsonNode>> propertyStream() for ObjectNode entries; empty Stream for other node types
    • forEachEntry(BiConsumer<? super String, ? super JsonNode>) for iterating over ObjectNode properties; NOP for other node types

In future we could also consider "parallel" variants for streams, and "spliterator".

Changes: Additional mutation (IN-PROGRESS)

Removal from Containers (PLANNED, 2.19)

  • databind#4955: add to ContainerNode (ObjectNode, ArrayNode):
    • removeIf(Predicate<? super JsonNode>): general purpose
    • removeNulls() (short-cut for removeIf(JsonNode::isNullNode))
    • removeEmpty() (short-cut for removeIf(JsonNode::isEmpty))

Changes: Transformations (COMPLETE, 2.19)

Additional asXxx() method(s) (COMPLETE, 2.19)

  • databind#4867 (COMPLETE, 2.19)
    • JsonNode.asOptional() which would give non-empty Optional<JsonNode> for all nodes EXCEPT for MissingNode for which Optional.empty() would be returned
  • databind#4868
    • ArrayNode JsonNode.asArray() to mean "if ArrayNode return as-is -- if not, wrap as 1-element ArrayNode" (or empty ArrayNode for `MissingNode)
    • Decided against implementation (due to no access to JsonNodeFactory).

Changes: Extended set of scalar value accessors (IN PROGRESS, 3.0)

  • databind#4958
    • Improved behavior of things like JsonNode.asInt(); extended/consistent set

Changes: Misc added in 2.10

  • JsonNode.toString() will guarantee valid JSON output (assuming default vanilla ObjectMapper settings)
    • JsonNode.isEmpty() works as alias for idiom size() == 0

Changes: "required" methods (added in 2.10)

(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 with JsonPointer
  • require() -- returns JsonNode (or subtype, co-variant) unless MissingNode; if MissingNode, throw exception (unchecked, Jackson-specific)
  • requireNonNull() -- same as require(), but fail both for MissingNode and NullNode