diff --git a/release-notes/VERSION b/release-notes/VERSION index 3f570832ff..fededd6f9b 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -17,6 +17,7 @@ Versions: 3.x (for earlier see VERSION-2.x) #5270: Remove `ObjectMapper.setSerializationInclusion()` from 3.0 (deprecate in 2.20) #5272: Replace `ObjectMapper.getRegisteredModuleIds()` (2.x) with `ObjectMapper.getRegisteredModules()` (3.0) +#5287: Change JsonNode.stringValue() of NullNode to return null, not fail (3.0) 3.0.0-rc8 (13-Aug-2025) diff --git a/src/main/java/tools/jackson/databind/JsonNode.java b/src/main/java/tools/jackson/databind/JsonNode.java index 46ef52ae58..6ad0613ab4 100644 --- a/src/main/java/tools/jackson/databind/JsonNode.java +++ b/src/main/java/tools/jackson/databind/JsonNode.java @@ -545,36 +545,41 @@ public Optional asOptional() { /** * Method that will try to access value of this node as a Java {@code String} - * which works if (and only if) node contains JSON String value: + * which works if (and only if) node contains JSON String or {@code null} value: * if not, a {@link JsonNodeException} will be thrown. + * In case of JSON {@code null}, Java {@code null} is returned. *

- * NOTE: for more lenient conversions, use {@link #asString()} + * NOTE: for more conversions, use {@link #asString()} instead. *

- * NOTE: in Jackson 2.x, was {@code textValue()}. + * NOTE: in Jackson 2.x, this method was named {@code textValue()}. * - * @return {@code String} value this node represents (if JSON String) + * @return {@code String} value this node represents (if JSON String), + * {@code null} for JSON {@code null} * - * @throws JsonNodeException if node value is not a JSON String value + * @throws JsonNodeException if node value is not a JSON String or Null value */ public abstract String stringValue(); /** * Method similar to {@link #stringValue()}, but that will return specified - * {@code defaultValue} if this node does not contain a JSON String. + * {@code defaultValue} if this node does not contain a JSON String. This + * default value case includes JSON {@code null}. * * @param defaultValue Value to return if this node does not contain a JSON String. * * @return Java {@code String} value this node represents (if JSON String); - * {@code defaultValue} otherwise + * {@code defaultValue} otherwise -- only returns {@code null} if {@code defaultValue} + * is {@code null} */ public abstract String stringValue(String defaultValue); /** * Method similar to {@link #stringValue()}, but that will return - * {@code Optional.empty()} if this node does not contain a JSON String. + * {@code Optional.empty()} if this node does not contain a JSON String + * (NOTE: JSON null is not considered a String here) * * @return {@code Optional} value (if node represents JSON String); - * {@code Optional.empty()} otherwise + * {@code Optional.empty()} otherwise (including for JSON null) */ public abstract Optional stringValueOpt(); diff --git a/src/main/java/tools/jackson/databind/node/NullNode.java b/src/main/java/tools/jackson/databind/node/NullNode.java index 19676913b1..c18c4ce470 100644 --- a/src/main/java/tools/jackson/databind/node/NullNode.java +++ b/src/main/java/tools/jackson/databind/node/NullNode.java @@ -59,6 +59,19 @@ protected String _asString() { return ""; } + // Explicit overrides for all overloads for documentation purposes + + @Override + public String stringValue() { return null; } + + @Override + public String stringValue(String defaultValue) { return defaultValue; } + + @Override + public Optional stringValueOpt() { + return Optional.empty(); + } + /* /********************************************************************** /* Overridden JsonNode methods, scalar access, numeric diff --git a/src/test/java/tools/jackson/databind/node/JsonNodeStringValueTest.java b/src/test/java/tools/jackson/databind/node/JsonNodeStringValueTest.java index 24e04803af..7d6e40f4ce 100644 --- a/src/test/java/tools/jackson/databind/node/JsonNodeStringValueTest.java +++ b/src/test/java/tools/jackson/databind/node/JsonNodeStringValueTest.java @@ -11,7 +11,7 @@ import tools.jackson.databind.testutil.DatabindTestUtil; import tools.jackson.databind.util.RawValue; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; /** @@ -65,10 +65,20 @@ public void stringValueFromStructural() @Test public void stringValueFromNonNumberMisc() { - _assertStringValueFailForNonString(NODES.nullNode()); _assertStringValueFailForNonString(NODES.missingNode()); } + @Test + public void stringValueFromNullNode() + { + JsonNode node = NODES.nullNode(); + assertEquals(null, node.stringValue()); + + // But also check defaulting + assertEquals("foo", node.stringValue("foo")); + assertFalse(node.stringValueOpt().isPresent()); + } + // // // asString() tests @Test