From 295c87752655b1efbc3381f4077b0e03e188e7ca Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 16 Dec 2024 23:59:36 +1300 Subject: [PATCH 1/2] [json-node] Fix NULL value bug with JsonObject Also fix DecimalAdapter --- .../io/avaje/json/node/adapter/DecimalAdapter.java | 12 ++++++------ .../java/io/avaje/json/node/adapter/NodeAdapter.java | 4 ++-- .../test/java/io/avaje/json/node/JsonObjectTest.java | 11 ++++++++++- .../json/node/adapter/JsonNodeAdaptersTest.java | 10 +++++++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java index d7025160..71844df9 100644 --- a/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java +++ b/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java @@ -3,17 +3,17 @@ import io.avaje.json.JsonAdapter; import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; -import io.avaje.json.node.JsonLong; +import io.avaje.json.node.JsonDecimal; -final class DecimalAdapter implements JsonAdapter { +final class DecimalAdapter implements JsonAdapter { @Override - public void toJson(JsonWriter writer, JsonLong value) { - writer.value(value.longValue()); + public void toJson(JsonWriter writer, JsonDecimal value) { + writer.value(value.decimalValue()); } @Override - public JsonLong fromJson(JsonReader reader) { - return JsonLong.of(reader.readLong()); + public JsonDecimal fromJson(JsonReader reader) { + return JsonDecimal.of(reader.readDecimal()); } } diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java index 956113af..f2734ec1 100644 --- a/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java +++ b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java @@ -33,6 +33,7 @@ ObjectAdapter objectAdapter() { public JsonNode fromJson(JsonReader reader) { switch (reader.currentToken()) { case NULL: + reader.isNullValue(); return null; case BEGIN_ARRAY: return arrayAdapter.fromJson(reader); @@ -55,8 +56,7 @@ public void toJson(JsonWriter writer, JsonNode value) { writer.nullValue(); return; } - JsonNode.Type type = value.type(); - switch (type) { + switch (value.type()) { case NULL: writer.nullValue(); break; diff --git a/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java index b4ee01f4..06d88ed6 100644 --- a/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java +++ b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java @@ -19,7 +19,7 @@ class JsonObjectTest { @Test void of() { - Map map = new LinkedHashMap<>(); + Map map = new LinkedHashMap<>(); map.put("name", JsonString.of("foo")); map.put("other", JsonInteger.of(42)); @@ -190,4 +190,13 @@ void toPlain() { assertThat(plainMap.get("name")).isEqualTo("foo"); assertThat(plainMap.get("other")).isEqualTo(Map.of("b", 42)); } + + @Test + void nullValuesInMap() { + String s = "{\"a\":1,\"b\":null,\"c\":null,\"d\":4}"; + JsonNodeMapper mapper = JsonNodeMapper.builder().build(); + JsonObject jsonObject = mapper.fromJsonObject(s); + + assertThat(jsonObject.elements().keySet()).containsExactly("a", "b", "c", "d"); + } } diff --git a/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java index 3f8b097a..84538306 100644 --- a/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java +++ b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java @@ -3,9 +3,11 @@ import io.avaje.json.JsonAdapter; import io.avaje.json.JsonReader; import io.avaje.json.node.*; +import io.avaje.json.simple.SimpleMapper; import io.avaje.json.stream.JsonStream; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -67,9 +69,15 @@ void create_JsonDouble_expect_sameInstance() { @Test void create_JsonDecimal_expect_sameInstance() { - JsonAdapter jsonAdapter = mapper.adapter(JsonDecimal.class); + JsonAdapter jsonAdapter = mapper.adapter(JsonDecimal.class); JsonAdapter adapter = mapper.adapter(JsonDecimal.class); assertThat(jsonAdapter).isSameAs(adapter); + + SimpleMapper.Type type = mapper.type(jsonAdapter); + + String asJson1 = type.toJson(JsonDecimal.of(new BigDecimal("23.45"))); + assertThat(asJson1).isEqualTo("23.45"); + assertThat(type.fromJson(asJson1)).isEqualTo(JsonDecimal.of(new BigDecimal("23.45"))); } @Test From a2ea03f3a2f81fe522c9fdfdc88400ce0a805b43 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Dec 2024 00:11:34 +1300 Subject: [PATCH 2/2] [json-node] Add JsonObject.extractOrEmpty() methods Returning Optional especially for String values mapping to other types. --- .../main/java/io/avaje/json/node/JsonNode.java | 17 +++++++++++++++++ .../java/io/avaje/json/node/JsonObject.java | 17 +++++++++++++---- .../io/avaje/json/node/ExtractNodeTest.java | 10 ++++++++++ .../java/io/avaje/json/node/JsonObjectTest.java | 3 +++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/json-node/src/main/java/io/avaje/json/node/JsonNode.java b/json-node/src/main/java/io/avaje/json/node/JsonNode.java index 41b96bbe..67e05303 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonNode.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonNode.java @@ -3,6 +3,7 @@ import org.jspecify.annotations.Nullable; import java.io.Serializable; +import java.util.Optional; /** * Represents the core JSON types. @@ -107,6 +108,13 @@ default String extract(String path) { throw new UnsupportedOperationException(); } + /** + * Extract the text from the given path if present else empty. + */ + default Optional extractOrEmpty(String path) { + throw new UnsupportedOperationException(); + } + /** * Extract the text from the given path if present or the given default value. * @@ -172,4 +180,13 @@ default JsonNode extractNode(String path, JsonNode missingValue) { throw new UnsupportedOperationException(); } + /** + * Extract the node from the given path if present else empty. + * + * @return The node for the given path. + */ + default Optional extractNodeOrEmpty(String path) { + throw new UnsupportedOperationException(); + } + } diff --git a/json-node/src/main/java/io/avaje/json/node/JsonObject.java b/json-node/src/main/java/io/avaje/json/node/JsonObject.java index 214674f0..11ff645b 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonObject.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonObject.java @@ -2,10 +2,7 @@ import org.jspecify.annotations.Nullable; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; @@ -220,6 +217,12 @@ public String extract(String path) { return node.text(); } + @Override + public Optional extractOrEmpty(String path) { + final var name = find(path); + return name == null ? Optional.empty() : Optional.of(name.text()); + } + @Override public String extract(String path, String missingValue) { final var name = find(path); @@ -272,4 +275,10 @@ public JsonNode extractNode(String path, JsonNode missingValue) { final var node = find(path); return node != null ? node : missingValue; } + + @Override + public Optional extractNodeOrEmpty(String path) { + final var node = find(path); + return Optional.ofNullable(node); + } } diff --git a/json-node/src/test/java/io/avaje/json/node/ExtractNodeTest.java b/json-node/src/test/java/io/avaje/json/node/ExtractNodeTest.java index f3702408..20e984f0 100644 --- a/json-node/src/test/java/io/avaje/json/node/ExtractNodeTest.java +++ b/json-node/src/test/java/io/avaje/json/node/ExtractNodeTest.java @@ -75,6 +75,16 @@ void extract() { assertThat(missingOther).hasSize(2); assertThat(missingOther).containsExactly("BOther", "MISSING!"); + + List missingOther2 = + arrayWithNestedPerson.stream() + .filter(node -> "family".equals(node.extract("type"))) + .flatMap(node -> node.extractNodeOrEmpty("person.other").stream()) + .map(JsonNode::text) + .collect(Collectors.toList()); + + assertThat(missingOther2).hasSize(1); + assertThat(missingOther2).containsExactly("BOther"); } @Test diff --git a/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java index 06d88ed6..f27784af 100644 --- a/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java +++ b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java @@ -141,6 +141,9 @@ void findNested() { assertThat(node.extract("person.active", false)).isEqualTo(true); assertThat(node.extract("person.missing", false)).isEqualTo(false); + assertThat(node.extractOrEmpty("person.missing")).isEmpty(); + assertThat(node.extractOrEmpty("person.name")).isNotEmpty().asString().contains("myName"); + assertThat(node.extract("address.size", -1)).isEqualTo(42); assertThat(node.extract("address.junk", -1L)).isEqualTo(99L); assertThat(node.extract("address.notSize", -1)).isEqualTo(-1);