From 2d345a0d9a35288bbc1587d2d356746d1b274b90 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 15:15:50 +0100 Subject: [PATCH 01/26] poc for generic nodes with streaming apis --- .../harrel/json/providers/jackson3/Deser.java | 178 ++++++++++++++++++ .../json/providers/jackson3/Jackson3Node.java | 21 ++- .../json/providers/jackson3/Jackson3Test.java | 7 +- 3 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java new file mode 100644 index 00000000..12e0d78d --- /dev/null +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java @@ -0,0 +1,178 @@ +package dev.harrel.json.providers.jackson3; + +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.AbstractJsonNode; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.function.Consumer; + +public class Deser extends ValueDeserializer { + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws JacksonException { + return readNode(p, ""); + } + + @Override + public JsonNode getNullValue(DeserializationContext ctx) { + return new GenericNode("", SimpleType.NULL, null); + } + + private JsonNode readNode(JsonParser p, String jsonPointer) { + return switch (p.currentToken()) { + case VALUE_NULL -> new GenericNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE, VALUE_FALSE -> new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING -> new GenericNode(jsonPointer, SimpleType.STRING, p.getString()); + case VALUE_NUMBER_INT -> new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NUMBER_FLOAT -> readNumber(p,jsonPointer); + case START_ARRAY -> readArray(p, jsonPointer); + case START_OBJECT -> readObject(p, jsonPointer); + case NOT_AVAILABLE, + END_OBJECT, + END_ARRAY, + PROPERTY_NAME, + VALUE_EMBEDDED_OBJECT -> throw new UnsupportedOperationException(p.currentToken().name()); + }; + } + + private JsonNode readNumber(JsonParser p, String jsonPointer) { + BigDecimal val = p.getDecimalValue(); + if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { + return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + } else { + return new GenericNode(jsonPointer, SimpleType.NUMBER, val); + } + } + + private JsonNode readArray(JsonParser p, String jsonPointer) { + List arr = new ArrayList<>(); + while (p.nextToken() != JsonToken.END_ARRAY) { + arr.add(readNode(p, jsonPointer + "/" + arr.size())); + } + return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); + } + + private JsonNode readObject(JsonParser p, String jsonPointer) { + Map obj = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String name = p.currentName(); + p.nextToken(); + obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); + } +} + +class GenericNode implements JsonNode { + final String jsonPointer; + final SimpleType type; + final Object value; + Object altNumber; + + public GenericNode(String jsonPointer, SimpleType type, Object value) { + this.jsonPointer = jsonPointer; + this.type = type; + this.value = value; + } + + @Override + public String getJsonPointer() { + return jsonPointer; + } + + @Override + public SimpleType getNodeType() { + return type; + } + + @Override + public boolean asBoolean() { + return (Boolean) value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public BigInteger asInteger() { + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (altNumber == null) { + altNumber = ((BigDecimal) value).toBigInteger(); + } + return (BigInteger) altNumber; + } + + @Override + public BigDecimal asNumber() { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (altNumber == null) { + altNumber = new BigDecimal((BigInteger) value); + } + return (BigDecimal) altNumber; + } + + @Override + public List asArray() { + return (List) value; + } + + @Override + public Map asObject() { + return (Map) value; + } + + @Override + public String toString() { + return "[%s, %s] %s".formatted(type, jsonPointer.isBlank() ? "/" : jsonPointer, value); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof GenericNode that)) { + return false; + } + return type == that.type && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(type, value); + } + + GenericNode copy(String jsonPointer) { + if (isArray()) { + List li = (List) value; + List copy = new ArrayList<>(li.size()); + for (int i = 0; i < li.size(); i++) { + copy.add(li.get(i).copy(jsonPointer + "/" + i)); + } + return new GenericNode(jsonPointer, type, copy); + } else if (isObject()) { + Map map = (Map) value; + Map copy = newHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + copy.put(entry.getKey(), entry.getValue().copy(jsonPointer + "/" + JsonNode.encodeJsonPointer(entry.getKey()))); + } + return new GenericNode(jsonPointer, type, copy); + } else { + return new GenericNode(jsonPointer, type, value); + } + } + + private static HashMap newHashMap(int realCapacity) { + return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); + } +} diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index 3fbb2450..a349719e 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -7,6 +7,7 @@ import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; import java.math.BigDecimal; import java.util.*; @@ -66,20 +67,26 @@ protected SimpleType computeNodeType(tools.jackson.databind.JsonNode node) { } public static final class Factory implements JsonNodeFactory { - private final ObjectMapper mapper; + private final JsonMapper mapper; public Factory() { this(JsonMapper.builder().enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS).build()); } public Factory(ObjectMapper mapper) { - this.mapper = mapper; + SimpleModule module = new SimpleModule(); + module.addDeserializer(JsonNode.class, new Deser()); + this.mapper = JsonMapper.builder().addModule(module).build(); } @Override - public Jackson3Node wrap(Object node) { - if (node instanceof Jackson3Node providerNode) { - return providerNode.jsonPointer.isEmpty() ? providerNode : new Jackson3Node(providerNode.node); + public JsonNode wrap(Object node) { + if (node instanceof GenericNode providerNode) { + if (providerNode.jsonPointer.isEmpty()) { + return providerNode; + } else { + return providerNode.copy(""); + } } else if (node instanceof tools.jackson.databind.JsonNode providerNode) { return new Jackson3Node(providerNode); } else { @@ -88,8 +95,8 @@ public Jackson3Node wrap(Object node) { } @Override - public Jackson3Node create(String rawJson) { - return new Jackson3Node(mapper.readTree(rawJson)); + public JsonNode create(String rawJson) { + return mapper.readValue(rawJson, JsonNode.class); } } } diff --git a/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java b/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java index 890c5e13..84feb761 100644 --- a/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java +++ b/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java @@ -1,9 +1,6 @@ package dev.harrel.json.providers.jackson3; -import dev.harrel.jsonschema.JsonNodeFactory; -import dev.harrel.jsonschema.ProviderTestBundle; -import dev.harrel.jsonschema.SimpleType; -import dev.harrel.jsonschema.ValidatorFactory; +import dev.harrel.jsonschema.*; import dev.harrel.jsonschema.providers.GsonNode; import dev.harrel.jsonschema.util.JsonNodeMock; import org.assertj.core.api.AssertionsForClassTypes; @@ -43,7 +40,7 @@ void shouldFailForGsonFactory() { @Test void shouldWrapForValidArgument() { tools.jackson.databind.JsonNode object = new ObjectMapper().readTree("{}"); - Jackson3Node wrap = new Jackson3Node.Factory().wrap(object); + JsonNode wrap = new Jackson3Node.Factory().wrap(object); assertThat(wrap).isNotNull(); assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); } From 2f3e02d78890a32e2e99c3575ad7cbb6626dfaa1 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 15:38:00 +0100 Subject: [PATCH 02/26] rearrange --- .../jsonschema/internal/GenericNode.java | 116 +++++++++++++++++ .../harrel/json/providers/jackson3/Deser.java | 123 ++---------------- 2 files changed, 125 insertions(+), 114 deletions(-) create mode 100644 lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java new file mode 100644 index 00000000..6164388c --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -0,0 +1,116 @@ +package dev.harrel.jsonschema.internal; + +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +/** + * Internal base class for all JSON provider implementations. + * Not part of the contract and not intend for external use. + */ +public final class GenericNode implements JsonNode { + private final String jsonPointer; + private final SimpleType type; + private final Object value; + private Object altNumber; + + public GenericNode(String jsonPointer, SimpleType type, Object value) { + this.jsonPointer = jsonPointer; + this.type = type; + this.value = value; + } + + @Override + public String getJsonPointer() { + return jsonPointer; + } + + @Override + public SimpleType getNodeType() { + return type; + } + + @Override + public boolean asBoolean() { + return (Boolean) value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public BigInteger asInteger() { + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (altNumber == null) { + altNumber = ((BigDecimal) value).toBigInteger(); + } + return (BigInteger) altNumber; + } + + @Override + public BigDecimal asNumber() { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (altNumber == null) { + altNumber = new BigDecimal((BigInteger) value); + } + return (BigDecimal) altNumber; + } + + @Override + public List asArray() { + return (List) value; + } + + @Override + public Map asObject() { + return (Map) value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof GenericNode)) { + return false; + } + GenericNode that = (GenericNode) o; + return type == that.type && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(type, value); + } + + GenericNode copy(String jsonPointer) { + if (isArray()) { + List li = (List) value; + List copy = new ArrayList<>(li.size()); + for (int i = 0; i < li.size(); i++) { + copy.add(li.get(i).copy(jsonPointer + "/" + i)); + } + return new GenericNode(jsonPointer, type, copy); + } else if (isObject()) { + Map map = (Map) value; + Map copy = newHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + copy.put(entry.getKey(), entry.getValue().copy(jsonPointer + "/" + JsonNode.encodeJsonPointer(entry.getKey()))); + } + return new GenericNode(jsonPointer, type, copy); + } else { + return new GenericNode(jsonPointer, type, value); + } + } + + // todo reuse + private static HashMap newHashMap(int realCapacity) { + return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); + } +} diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java index 12e0d78d..648c8f1e 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java @@ -2,7 +2,7 @@ import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.SimpleType; -import dev.harrel.jsonschema.internal.AbstractJsonNode; +import dev.harrel.jsonschema.internal.GenericNode; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; @@ -10,11 +10,12 @@ import tools.jackson.databind.ValueDeserializer; import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.*; -import java.util.function.Consumer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; -public class Deser extends ValueDeserializer { +public final class Deser extends ValueDeserializer { @Override public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws JacksonException { return readNode(p, ""); @@ -31,19 +32,20 @@ private JsonNode readNode(JsonParser p, String jsonPointer) { case VALUE_TRUE, VALUE_FALSE -> new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); case VALUE_STRING -> new GenericNode(jsonPointer, SimpleType.STRING, p.getString()); case VALUE_NUMBER_INT -> new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); - case VALUE_NUMBER_FLOAT -> readNumber(p,jsonPointer); + case VALUE_NUMBER_FLOAT -> readNumber(p, jsonPointer); case START_ARRAY -> readArray(p, jsonPointer); case START_OBJECT -> readObject(p, jsonPointer); case NOT_AVAILABLE, END_OBJECT, END_ARRAY, PROPERTY_NAME, - VALUE_EMBEDDED_OBJECT -> throw new UnsupportedOperationException(p.currentToken().name()); + VALUE_EMBEDDED_OBJECT -> throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg }; } private JsonNode readNumber(JsonParser p, String jsonPointer) { BigDecimal val = p.getDecimalValue(); + // todo reuse if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); } else { @@ -69,110 +71,3 @@ private JsonNode readObject(JsonParser p, String jsonPointer) { return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); } } - -class GenericNode implements JsonNode { - final String jsonPointer; - final SimpleType type; - final Object value; - Object altNumber; - - public GenericNode(String jsonPointer, SimpleType type, Object value) { - this.jsonPointer = jsonPointer; - this.type = type; - this.value = value; - } - - @Override - public String getJsonPointer() { - return jsonPointer; - } - - @Override - public SimpleType getNodeType() { - return type; - } - - @Override - public boolean asBoolean() { - return (Boolean) value; - } - - @Override - public String asString() { - return String.valueOf(value); - } - - @Override - public BigInteger asInteger() { - if (value instanceof BigInteger) { - return (BigInteger) value; - } - if (altNumber == null) { - altNumber = ((BigDecimal) value).toBigInteger(); - } - return (BigInteger) altNumber; - } - - @Override - public BigDecimal asNumber() { - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - if (altNumber == null) { - altNumber = new BigDecimal((BigInteger) value); - } - return (BigDecimal) altNumber; - } - - @Override - public List asArray() { - return (List) value; - } - - @Override - public Map asObject() { - return (Map) value; - } - - @Override - public String toString() { - return "[%s, %s] %s".formatted(type, jsonPointer.isBlank() ? "/" : jsonPointer, value); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof GenericNode that)) { - return false; - } - return type == that.type && Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(type, value); - } - - GenericNode copy(String jsonPointer) { - if (isArray()) { - List li = (List) value; - List copy = new ArrayList<>(li.size()); - for (int i = 0; i < li.size(); i++) { - copy.add(li.get(i).copy(jsonPointer + "/" + i)); - } - return new GenericNode(jsonPointer, type, copy); - } else if (isObject()) { - Map map = (Map) value; - Map copy = newHashMap(map.size()); - for (Map.Entry entry : map.entrySet()) { - copy.put(entry.getKey(), entry.getValue().copy(jsonPointer + "/" + JsonNode.encodeJsonPointer(entry.getKey()))); - } - return new GenericNode(jsonPointer, type, copy); - } else { - return new GenericNode(jsonPointer, type, value); - } - } - - private static HashMap newHashMap(int realCapacity) { - return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); - } -} From d7188dfee8aaac9e6085d79d3fae8d42ed44cbea Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 15:47:44 +0100 Subject: [PATCH 03/26] rearrange 2 --- .../jackson3/{Deser.java => Jackson3Deserializer.java} | 2 +- .../java/dev/harrel/json/providers/jackson3/Jackson3Node.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/{Deser.java => Jackson3Deserializer.java} (97%) diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java similarity index 97% rename from providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java rename to providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java index 648c8f1e..09fe7da7 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Deser.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java @@ -15,7 +15,7 @@ import java.util.List; import java.util.Map; -public final class Deser extends ValueDeserializer { +public final class Jackson3Deserializer extends ValueDeserializer { @Override public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws JacksonException { return readNode(p, ""); diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index a349719e..cb43bd55 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -75,7 +75,7 @@ public Factory() { public Factory(ObjectMapper mapper) { SimpleModule module = new SimpleModule(); - module.addDeserializer(JsonNode.class, new Deser()); + module.addDeserializer(JsonNode.class, new Jackson3Deserializer()); this.mapper = JsonMapper.builder().addModule(module).build(); } From 8d8d83e748aae0e1589bd8cbc4c473d55366f62b Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 15:50:49 +0100 Subject: [PATCH 04/26] fix compilation --- .../java/dev/harrel/jsonschema/internal/GenericNode.java | 2 +- .../dev/harrel/json/providers/jackson3/Jackson3Node.java | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java index 6164388c..edbbe0c6 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -89,7 +89,7 @@ public int hashCode() { return Objects.hash(type, value); } - GenericNode copy(String jsonPointer) { + public GenericNode copy(String jsonPointer) { if (isArray()) { List li = (List) value; List copy = new ArrayList<>(li.size()); diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index cb43bd55..ac4d3e44 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -4,6 +4,7 @@ import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; +import dev.harrel.jsonschema.internal.GenericNode; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; @@ -81,11 +82,11 @@ public Factory(ObjectMapper mapper) { @Override public JsonNode wrap(Object node) { - if (node instanceof GenericNode providerNode) { - if (providerNode.jsonPointer.isEmpty()) { - return providerNode; + if (node instanceof GenericNode genericNode) { + if (genericNode.getJsonPointer().isEmpty()) { + return genericNode; } else { - return providerNode.copy(""); + return genericNode.copy(""); } } else if (node instanceof tools.jackson.databind.JsonNode providerNode) { return new Jackson3Node(providerNode); From 08ce5f81affbfb3c734ad562f000d4c7985f7fc0 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 16:04:47 +0100 Subject: [PATCH 05/26] stream api for jackson 2.x --- .../integration/jacksonTest/JacksonTest.java | 2 +- .../jsonschema/providers/JacksonNode.java | 91 ++++++++++++++++++- .../jsonschema/JsonNodeFactoriesTest.java | 4 +- .../harrel/jsonschema/SchemaRegistryTest.java | 2 +- .../json/providers/jackson3/Jackson3Node.java | 1 + 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/lib/src/integration/jacksonTest/JacksonTest.java b/lib/src/integration/jacksonTest/JacksonTest.java index 8d206a13..226a51b9 100644 --- a/lib/src/integration/jacksonTest/JacksonTest.java +++ b/lib/src/integration/jacksonTest/JacksonTest.java @@ -49,7 +49,7 @@ void shouldFailForGsonFactory() { @Test void shouldWrapForValidArgument() throws JsonProcessingException { JsonNode object = new ObjectMapper().readTree("{}"); - JacksonNode wrap = new JacksonNode.Factory().wrap(object); + dev.harrel.jsonschema.JsonNode wrap = new JacksonNode.Factory().wrap(object); assertThat(wrap).isNotNull(); assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 805252e3..979e69d5 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -1,11 +1,18 @@ package dev.harrel.jsonschema.providers; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; +import dev.harrel.jsonschema.internal.GenericNode; import java.io.IOException; import java.math.BigDecimal; @@ -74,14 +81,23 @@ public Factory() { } public Factory(ObjectMapper mapper) { - this.mapper = mapper; + SimpleModule module = new SimpleModule(); + module.addDeserializer(JsonNode.class, new JacksonDeserializer()); + this.mapper = mapper.registerModule(module); } @Override - public JacksonNode wrap(Object node) { + public JsonNode wrap(Object node) { if (node instanceof JacksonNode) { JacksonNode providerNode = (JacksonNode) node; return providerNode.jsonPointer.isEmpty() ? providerNode : new JacksonNode((providerNode).node); + } else if (node instanceof GenericNode) { + GenericNode genericNode = (GenericNode) node; + if (genericNode.getJsonPointer().isEmpty()) { + return genericNode; + } else { + return genericNode.copy(""); + } } else if (node instanceof com.fasterxml.jackson.databind.JsonNode) { return new JacksonNode((com.fasterxml.jackson.databind.JsonNode) node); } else { @@ -90,12 +106,79 @@ public JacksonNode wrap(Object node) { } @Override - public JacksonNode create(String rawJson) { + public JsonNode create(String rawJson) { try { - return new JacksonNode(mapper.readTree(rawJson)); + return mapper.readValue(rawJson, JsonNode.class); } catch (IOException e) { throw new IllegalArgumentException(e); } } } } + +final class JacksonDeserializer extends JsonDeserializer { + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + return readNode(p, ""); + } + + @Override + public JsonNode getNullValue(DeserializationContext ctx) { + return new GenericNode("", SimpleType.NULL, null); + } + + private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { + switch (p.currentToken()) { + case VALUE_NULL: + return new GenericNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE: + case VALUE_FALSE: + return new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING: + return new GenericNode(jsonPointer, SimpleType.STRING, p.getText()); + case VALUE_NUMBER_INT: + return new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NUMBER_FLOAT: + return readNumber(p, jsonPointer); + case START_ARRAY: + return readArray(p, jsonPointer); + case START_OBJECT: + return readObject(p, jsonPointer); + case NOT_AVAILABLE: + case END_OBJECT: + case END_ARRAY: + case FIELD_NAME: + case VALUE_EMBEDDED_OBJECT: + default: + throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg + } + } + + private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { + BigDecimal val = p.getDecimalValue(); + // todo reuse + if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { + return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + } else { + return new GenericNode(jsonPointer, SimpleType.NUMBER, val); + } + } + + private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException { + List arr = new ArrayList<>(); + while (p.nextToken() != JsonToken.END_ARRAY) { + arr.add(readNode(p, jsonPointer + "/" + arr.size())); + } + return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); + } + + private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { + Map obj = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String name = p.currentName(); + p.nextToken(); + obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); + } +} diff --git a/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java b/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java index 347b0613..5d3d6c28 100644 --- a/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java +++ b/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java @@ -116,7 +116,7 @@ void shouldUseSchemaFactoryWhenResolvingSchema() { .withSchemaResolver(resolver) .createValidator(); URI uri = URI.create("urn:test"); - JacksonNode jacksonNode = new JacksonNode.Factory().create(""" + JsonNode jacksonNode = new JacksonNode.Factory().create(""" { "$ref": "urn:x" }"""); @@ -133,7 +133,7 @@ void shouldUseSchemaFactoryWhenResolvingMetaSchema() { .withSchemaResolver(resolver) .createValidator(); URI uri = URI.create("urn:test"); - JacksonNode jacksonNode = new JacksonNode.Factory().create("{}"); + JsonNode jacksonNode = new JacksonNode.Factory().create("{}"); Exception exception = catchException(() -> validator.registerSchema(uri, jacksonNode)); assertThat(exception).isInstanceOf(MetaSchemaResolvingException.class); diff --git a/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java b/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java index 70b6f5ea..0a3f2a92 100644 --- a/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java +++ b/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java @@ -16,7 +16,7 @@ void shouldRestoreStateProperly() { SchemaRegistry schemaRegistry = new SchemaRegistry(); SchemaParsingContext ctx = new SchemaParsingContext(new MetaSchemaData(new Dialects.Draft2020Dialect()), URI.create("urn:test"), emptyMap()); JacksonNode.Factory factory = new JacksonNode.Factory(); - JacksonNode rootSchemaNode = factory.create(""" + JsonNode rootSchemaNode = factory.create(""" { "properties": { "field": true diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index ac4d3e44..655cade2 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -82,6 +82,7 @@ public Factory(ObjectMapper mapper) { @Override public JsonNode wrap(Object node) { + // todo support for old nodes as well if (node instanceof GenericNode genericNode) { if (genericNode.getJsonPointer().isEmpty()) { return genericNode; From cc435ba1459aa9e04e9d3a68b7c1362c64f386ff Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 21:05:47 +0100 Subject: [PATCH 06/26] full serde --- .../jsonschema/internal/GenericNode.java | 4 +- .../internal/InternalProviderUtil.java | 2 +- .../jsonschema/providers/JacksonNode.java | 68 +++++++++++++++---- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java index edbbe0c6..e63ae793 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -81,12 +81,12 @@ public boolean equals(Object o) { return false; } GenericNode that = (GenericNode) o; - return type == that.type && Objects.equals(value, that.value); + return Objects.equals(value, that.value); } @Override public int hashCode() { - return Objects.hash(type, value); + return Objects.hash(value); } public GenericNode copy(String jsonPointer) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java index 6c19dd7a..95572279 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java @@ -9,6 +9,6 @@ public final class InternalProviderUtil { private InternalProviderUtil() {} public static boolean canUseNativeEquals(JsonNode node) { - return node instanceof AbstractJsonNode; + return node instanceof GenericNode || node instanceof AbstractJsonNode; } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 979e69d5..7bec91b2 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -1,12 +1,7 @@ package dev.harrel.jsonschema.providers; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.module.SimpleModule; import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.JsonNodeFactory; @@ -73,17 +68,18 @@ protected SimpleType computeNodeType(com.fasterxml.jackson.databind.JsonNode nod } } - public static final class Factory implements JsonNodeFactory { + public static final class Factory extends SimpleModule implements JsonNodeFactory { private final ObjectMapper mapper; public Factory() { - this(new ObjectMapper().enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)); + this(new ObjectMapper()); } public Factory(ObjectMapper mapper) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(JsonNode.class, new JacksonDeserializer()); - this.mapper = mapper.registerModule(module); + super(Factory.class.getName(), Version.unknownVersion(), + Collections.singletonMap(JsonNode.class, new JacksonDeserializer()), + Collections.singletonList(new JacksonSerializer())); + this.mapper = mapper.copy().registerModule(this); } @Override @@ -182,3 +178,51 @@ private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); } } + +final class JacksonSerializer extends JsonSerializer { + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + writeNode(value, gen); + } + + @Override + public Class handledType() { + return JsonNode.class; + } + + void writeNode(JsonNode value, JsonGenerator gen) throws IOException { + switch (value.getNodeType()) { + case NULL: + gen.writeNull(); + return; + case BOOLEAN: + gen.writeBoolean(value.asBoolean()); + return; + case STRING: + gen.writeString(value.asString()); + return; + case INTEGER: + gen.writeNumber(value.asInteger()); + return; + case NUMBER: + gen.writeNumber(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + gen.writeStartArray(null, arr.size()); + for (int i = 0; i < arr.size(); i++) { + writeNode(arr.get(i), gen); + } + gen.writeEndArray(); + return; + case OBJECT: + Map map = value.asObject(); + gen.writeStartObject(null, map.size()); + for (Map.Entry entry : map.entrySet()) { + gen.writeFieldName(entry.getKey()); + writeNode(entry.getValue(), gen); + } + gen.writeEndObject(); + } + } +} From 9d2a1bc891f4622d827f603286aaba8fe16e39d3 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 21:11:49 +0100 Subject: [PATCH 07/26] use older API to keep jackson 2.2 support --- .../java/dev/harrel/jsonschema/providers/JacksonNode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 7bec91b2..cb5d8bcf 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -119,12 +119,12 @@ public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOE } @Override - public JsonNode getNullValue(DeserializationContext ctx) { + public JsonNode getNullValue() { return new GenericNode("", SimpleType.NULL, null); } private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { - switch (p.currentToken()) { + switch (p.getCurrentToken()) { case VALUE_NULL: return new GenericNode(jsonPointer, SimpleType.NULL, null); case VALUE_TRUE: @@ -171,7 +171,7 @@ private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { Map obj = new LinkedHashMap<>(); while (p.nextToken() != JsonToken.END_OBJECT) { - String name = p.currentName(); + String name = p.getCurrentName(); p.nextToken(); obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); } From 3603bfc1d47662330aa52e8fd14ef01a8bf3b142 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 21:19:56 +0100 Subject: [PATCH 08/26] suppression, nullchecks --- .../java/dev/harrel/jsonschema/internal/GenericNode.java | 7 +++++-- .../java/dev/harrel/jsonschema/providers/JacksonNode.java | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java index e63ae793..59ad0109 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -18,8 +18,8 @@ public final class GenericNode implements JsonNode { private Object altNumber; public GenericNode(String jsonPointer, SimpleType type, Object value) { - this.jsonPointer = jsonPointer; - this.type = type; + this.jsonPointer = Objects.requireNonNull(jsonPointer); + this.type = Objects.requireNonNull(type); this.value = value; } @@ -66,11 +66,13 @@ public BigDecimal asNumber() { } @Override + @SuppressWarnings("unchecked") public List asArray() { return (List) value; } @Override + @SuppressWarnings("unchecked") public Map asObject() { return (Map) value; } @@ -89,6 +91,7 @@ public int hashCode() { return Objects.hash(value); } + @SuppressWarnings("unchecked") public GenericNode copy(String jsonPointer) { if (isArray()) { List li = (List) value; diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index cb5d8bcf..6c57b13c 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -32,6 +32,7 @@ protected List createArray() { } @Override + @SuppressWarnings("deprecation") protected Map createObject() { Map map = newHashMap(node.size()); for (Iterator> iterator = node.fields(); iterator.hasNext(); ) { @@ -119,6 +120,7 @@ public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOE } @Override + @SuppressWarnings("deprecation") public JsonNode getNullValue() { return new GenericNode("", SimpleType.NULL, null); } @@ -168,6 +170,7 @@ private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); } + @SuppressWarnings("deprecation") private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { Map obj = new LinkedHashMap<>(); while (p.nextToken() != JsonToken.END_OBJECT) { From e9501cb1be8688ddf42565d5a40ae4d27d0476f1 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 21:36:46 +0100 Subject: [PATCH 09/26] move helper methods --- .../harrel/jsonschema/internal/AbstractJsonNode.java | 8 -------- .../dev/harrel/jsonschema/internal/GenericNode.java | 7 ++----- .../jsonschema/internal/InternalProviderUtil.java | 11 +++++++++++ .../dev/harrel/jsonschema/providers/GsonNode.java | 3 +++ .../dev/harrel/jsonschema/providers/JacksonNode.java | 9 +++++++-- .../harrel/jsonschema/providers/JakartaJsonNode.java | 3 +++ .../dev/harrel/jsonschema/providers/JettisonNode.java | 2 ++ .../harrel/jsonschema/providers/JsonSmartNode.java | 1 + .../harrel/jsonschema/providers/KotlinxJsonNode.java | 3 +++ .../dev/harrel/jsonschema/providers/OrgJsonNode.java | 2 ++ .../harrel/jsonschema/providers/SimpleJsonNode.java | 2 ++ .../harrel/jsonschema/providers/SnakeYamlNode.java | 3 +++ .../harrel/json/providers/jackson3/Jackson3Node.java | 3 +++ 13 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java index f54aa446..ada1dbcf 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java @@ -124,12 +124,4 @@ private void ensureInitialized() { protected abstract List createArray(); protected abstract Map createObject(); protected abstract SimpleType computeNodeType(T node); - - protected static boolean canConvertToInteger(BigDecimal bigDecimal) { - return bigDecimal.scale() <= 0 || bigDecimal.stripTrailingZeros().scale() <= 0; - } - - protected static HashMap newHashMap(int realCapacity) { - return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); - } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java index 59ad0109..4b715a8e 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -7,6 +7,8 @@ import java.math.BigInteger; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + /** * Internal base class for all JSON provider implementations. * Not part of the contract and not intend for external use. @@ -111,9 +113,4 @@ public GenericNode copy(String jsonPointer) { return new GenericNode(jsonPointer, type, value); } } - - // todo reuse - private static HashMap newHashMap(int realCapacity) { - return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); - } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java index 95572279..d50fb8c4 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java @@ -2,6 +2,9 @@ import dev.harrel.jsonschema.JsonNode; +import java.math.BigDecimal; +import java.util.HashMap; + /** * Internal helper class. */ @@ -11,4 +14,12 @@ private InternalProviderUtil() {} public static boolean canUseNativeEquals(JsonNode node) { return node instanceof GenericNode || node instanceof AbstractJsonNode; } + + public static boolean canConvertToInteger(BigDecimal bigDecimal) { + return bigDecimal.scale() <= 0 || bigDecimal.stripTrailingZeros().scale() <= 0; + } + + public static HashMap newHashMap(int realCapacity) { + return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); + } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java index f02f243a..e09be32b 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java @@ -12,6 +12,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class GsonNode extends AbstractJsonNode { private GsonNode(JsonElement node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 6c57b13c..1aaeefd5 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -13,6 +13,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JacksonNode extends AbstractJsonNode { private JacksonNode(com.fasterxml.jackson.databind.JsonNode node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); @@ -31,6 +34,7 @@ protected List createArray() { return elements; } + /* Using deprecated API to support older versions as well */ @Override @SuppressWarnings("deprecation") protected Map createObject() { @@ -119,6 +123,7 @@ public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOE return readNode(p, ""); } + /* Using deprecated API to support older versions as well */ @Override @SuppressWarnings("deprecation") public JsonNode getNullValue() { @@ -154,8 +159,7 @@ private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { BigDecimal val = p.getDecimalValue(); - // todo reuse - if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { + if (canConvertToInteger(val)) { return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); } else { return new GenericNode(jsonPointer, SimpleType.NUMBER, val); @@ -170,6 +174,7 @@ private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); } + /* Using deprecated API to support older versions as well */ @SuppressWarnings("deprecation") private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { Map obj = new LinkedHashMap<>(); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java index c79ba5c3..692ffcd4 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java @@ -12,6 +12,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JakartaJsonNode extends AbstractJsonNode { private JakartaJsonNode(JsonValue node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java index 297929aa..e80de571 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java @@ -9,6 +9,8 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JettisonNode extends SimpleJsonNode { private JettisonNode(Object node, String jsonPointer) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java index 50842fe8..88d36346 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java @@ -9,6 +9,7 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; import static net.minidev.json.parser.JSONParser.MODE_JSON_SIMPLE; public final class JsonSmartNode extends SimpleJsonNode { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java index 5e64f12e..305e1950 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java @@ -9,6 +9,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class KotlinxJsonNode extends AbstractJsonNode { private KotlinxJsonNode(JsonElement node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java index b644af55..e2d10551 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java @@ -8,6 +8,8 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class OrgJsonNode extends SimpleJsonNode { private OrgJsonNode(Object node, String jsonPointer) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java index 1db31c5b..33ecf8e0 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java @@ -6,6 +6,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + abstract class SimpleJsonNode extends AbstractJsonNode { SimpleJsonNode(Object node, String jsonPointer) { super(node, jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java index 45450fa4..ab729a19 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java @@ -14,6 +14,9 @@ import java.math.BigInteger; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class SnakeYamlNode extends AbstractJsonNode { private static final SafeConstructor CONSTR = new SafeConstructor(new LoaderOptions()); private static final SafeConstructor.ConstructYamlBool BOOLEAN_CREATOR = CONSTR.new ConstructYamlBool(); diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index 655cade2..87401503 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -13,6 +13,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class Jackson3Node extends AbstractJsonNode { private Jackson3Node(tools.jackson.databind.JsonNode node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); From 4ddaac2275c7083d0ae924f502502b234b93a5ae Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 22:11:30 +0100 Subject: [PATCH 10/26] quirky eager class loading --- lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java index a7ddeaa7..d5b7384c 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java +++ b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java @@ -17,7 +17,7 @@ public final class ValidatorFactory { private final Map dialects = new HashMap<>(Dialects.OFFICIAL_DIALECTS); private Dialect defaultDialect = new Dialects.Draft2020Dialect(); private EvaluatorFactory evaluatorFactory; - private Supplier schemaNodeFactory = JacksonNode.Factory::new; + private Supplier schemaNodeFactory = () -> new JacksonNode.Factory(); // cannot be a method reference due to eager class loading private Supplier instanceNodeFactory = schemaNodeFactory; private SchemaResolver schemaResolver = new DefaultSchemaResolver(); private MessageProvider messageProvider = MessageProvider.fromLocale(Locale.getDefault()); From 0b5c73965aa4032fa9df52dac647f3e98662c514 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 15 Mar 2026 22:16:28 +0100 Subject: [PATCH 11/26] todos --- .../main/java/dev/harrel/jsonschema/providers/JacksonNode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 1aaeefd5..29c60154 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -73,6 +73,7 @@ protected SimpleType computeNodeType(com.fasterxml.jackson.databind.JsonNode nod } } + // todo doc about simple module public static final class Factory extends SimpleModule implements JsonNodeFactory { private final ObjectMapper mapper; @@ -80,6 +81,7 @@ public Factory() { this(new ObjectMapper()); } + @Deprecated // todo since and doc public Factory(ObjectMapper mapper) { super(Factory.class.getName(), Version.unknownVersion(), Collections.singletonMap(JsonNode.class, new JacksonDeserializer()), From 10e148249f43e397a168d8ee0c9afb1c7425dce3 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 17:03:06 +0200 Subject: [PATCH 12/26] start rearranging into JacksonModule --- .../jsonschema/internal/GenericNode.java | 2 +- .../jsonschema/providers/JacksonModule.java | 148 ++++++++++++++++++ .../jsonschema/providers/JacksonNode.java | 128 +-------------- 3 files changed, 152 insertions(+), 126 deletions(-) create mode 100644 lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java index 4b715a8e..27c868e4 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java @@ -11,7 +11,7 @@ /** * Internal base class for all JSON provider implementations. - * Not part of the contract and not intend for external use. + * Not part of the contract and not intended for external use. */ public final class GenericNode implements JsonNode { private final String jsonPointer; diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java new file mode 100644 index 00000000..df6031dd --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -0,0 +1,148 @@ +package dev.harrel.jsonschema.providers; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.GenericNode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; + +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + +// todo doc +public final class JacksonModule extends SimpleModule { + + public JacksonModule() { + super(JacksonModule.class.getName(), Version.unknownVersion(), + Collections.singletonMap(JsonNode.class, new Deserializer()), + Collections.singletonList(new Serializer())); + } + + static final class Deserializer extends JsonDeserializer { + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + return readNode(p, ""); + } + + /* Using deprecated API to support older versions as well */ + @Override + @SuppressWarnings("deprecation") + public JsonNode getNullValue() { + return new GenericNode("", SimpleType.NULL, null); + } + + private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { + switch (p.getCurrentToken()) { + case VALUE_NULL: + return new GenericNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE: + case VALUE_FALSE: + return new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING: + return new GenericNode(jsonPointer, SimpleType.STRING, p.getText()); + case VALUE_NUMBER_INT: + return new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NUMBER_FLOAT: + return readNumber(p, jsonPointer); + case START_ARRAY: + return readArray(p, jsonPointer); + case START_OBJECT: + return readObject(p, jsonPointer); + case NOT_AVAILABLE: + case END_OBJECT: + case END_ARRAY: + case FIELD_NAME: + case VALUE_EMBEDDED_OBJECT: + default: + throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg + } + } + + private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { + BigDecimal val = p.getDecimalValue(); + if (canConvertToInteger(val)) { + return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + } else { + return new GenericNode(jsonPointer, SimpleType.NUMBER, val); + } + } + + private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException { + List arr = new ArrayList<>(); + while (p.nextToken() != JsonToken.END_ARRAY) { + arr.add(readNode(p, jsonPointer + "/" + arr.size())); + } + return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); + } + + /* Using deprecated API to support older versions as well */ + @SuppressWarnings("deprecation") + private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { + Map obj = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String name = p.getCurrentName(); + p.nextToken(); + obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); + } + } + + static final class Serializer extends JsonSerializer { + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + writeNode(value, gen); + } + + @Override + public Class handledType() { + return JsonNode.class; + } + + void writeNode(JsonNode value, JsonGenerator gen) throws IOException { + switch (value.getNodeType()) { + case NULL: + gen.writeNull(); + return; + case BOOLEAN: + gen.writeBoolean(value.asBoolean()); + return; + case STRING: + gen.writeString(value.asString()); + return; + case INTEGER: + gen.writeNumber(value.asInteger()); + return; + case NUMBER: + gen.writeNumber(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + gen.writeStartArray(null, arr.size()); + for (int i = 0; i < arr.size(); i++) { + writeNode(arr.get(i), gen); + } + gen.writeEndArray(); + return; + case OBJECT: + Map map = value.asObject(); + gen.writeStartObject(null, map.size()); + for (Map.Entry entry : map.entrySet()) { + gen.writeFieldName(entry.getKey()); + writeNode(entry.getValue(), gen); + } + gen.writeEndObject(); + } + } + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 29c60154..71785805 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -73,20 +73,16 @@ protected SimpleType computeNodeType(com.fasterxml.jackson.databind.JsonNode nod } } - // todo doc about simple module - public static final class Factory extends SimpleModule implements JsonNodeFactory { + public static final class Factory implements JsonNodeFactory { private final ObjectMapper mapper; public Factory() { this(new ObjectMapper()); } - @Deprecated // todo since and doc + @Deprecated // todo since and doc, (should it be really deprecated?) public Factory(ObjectMapper mapper) { - super(Factory.class.getName(), Version.unknownVersion(), - Collections.singletonMap(JsonNode.class, new JacksonDeserializer()), - Collections.singletonList(new JacksonSerializer())); - this.mapper = mapper.copy().registerModule(this); + this.mapper = mapper.copy().registerModule(new JacksonModule()); } @Override @@ -118,121 +114,3 @@ public JsonNode create(String rawJson) { } } } - -final class JacksonDeserializer extends JsonDeserializer { - @Override - public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { - return readNode(p, ""); - } - - /* Using deprecated API to support older versions as well */ - @Override - @SuppressWarnings("deprecation") - public JsonNode getNullValue() { - return new GenericNode("", SimpleType.NULL, null); - } - - private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { - switch (p.getCurrentToken()) { - case VALUE_NULL: - return new GenericNode(jsonPointer, SimpleType.NULL, null); - case VALUE_TRUE: - case VALUE_FALSE: - return new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); - case VALUE_STRING: - return new GenericNode(jsonPointer, SimpleType.STRING, p.getText()); - case VALUE_NUMBER_INT: - return new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); - case VALUE_NUMBER_FLOAT: - return readNumber(p, jsonPointer); - case START_ARRAY: - return readArray(p, jsonPointer); - case START_OBJECT: - return readObject(p, jsonPointer); - case NOT_AVAILABLE: - case END_OBJECT: - case END_ARRAY: - case FIELD_NAME: - case VALUE_EMBEDDED_OBJECT: - default: - throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg - } - } - - private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { - BigDecimal val = p.getDecimalValue(); - if (canConvertToInteger(val)) { - return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); - } else { - return new GenericNode(jsonPointer, SimpleType.NUMBER, val); - } - } - - private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException { - List arr = new ArrayList<>(); - while (p.nextToken() != JsonToken.END_ARRAY) { - arr.add(readNode(p, jsonPointer + "/" + arr.size())); - } - return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); - } - - /* Using deprecated API to support older versions as well */ - @SuppressWarnings("deprecation") - private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { - Map obj = new LinkedHashMap<>(); - while (p.nextToken() != JsonToken.END_OBJECT) { - String name = p.getCurrentName(); - p.nextToken(); - obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); - } - return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); - } -} - -final class JacksonSerializer extends JsonSerializer { - @Override - public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - writeNode(value, gen); - } - - @Override - public Class handledType() { - return JsonNode.class; - } - - void writeNode(JsonNode value, JsonGenerator gen) throws IOException { - switch (value.getNodeType()) { - case NULL: - gen.writeNull(); - return; - case BOOLEAN: - gen.writeBoolean(value.asBoolean()); - return; - case STRING: - gen.writeString(value.asString()); - return; - case INTEGER: - gen.writeNumber(value.asInteger()); - return; - case NUMBER: - gen.writeNumber(value.asNumber()); - return; - case ARRAY: - List arr = value.asArray(); - gen.writeStartArray(null, arr.size()); - for (int i = 0; i < arr.size(); i++) { - writeNode(arr.get(i), gen); - } - gen.writeEndArray(); - return; - case OBJECT: - Map map = value.asObject(); - gen.writeStartObject(null, map.size()); - for (Map.Entry entry : map.entrySet()) { - gen.writeFieldName(entry.getKey()); - writeNode(entry.getValue(), gen); - } - gen.writeEndObject(); - } - } -} From c1d943251288096ce58759bebe33019196f52d94 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 17:06:47 +0200 Subject: [PATCH 13/26] revert lambda as no longer na issue --- lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java index d5b7384c..a7ddeaa7 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java +++ b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java @@ -17,7 +17,7 @@ public final class ValidatorFactory { private final Map dialects = new HashMap<>(Dialects.OFFICIAL_DIALECTS); private Dialect defaultDialect = new Dialects.Draft2020Dialect(); private EvaluatorFactory evaluatorFactory; - private Supplier schemaNodeFactory = () -> new JacksonNode.Factory(); // cannot be a method reference due to eager class loading + private Supplier schemaNodeFactory = JacksonNode.Factory::new; private Supplier instanceNodeFactory = schemaNodeFactory; private SchemaResolver schemaResolver = new DefaultSchemaResolver(); private MessageProvider messageProvider = MessageProvider.fromLocale(Locale.getDefault()); From b0c951873cd0df6a56b22ff7d2007816ca4eaa36 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 17:12:20 +0200 Subject: [PATCH 14/26] GenericNode -> StandaloneNode --- .../internal/InternalProviderUtil.java | 2 +- .../{GenericNode.java => StandaloneNode.java} | 26 +++++++++---------- .../jsonschema/providers/JacksonModule.java | 20 +++++++------- .../jsonschema/providers/JacksonNode.java | 14 +++++----- .../jackson3/Jackson3Deserializer.java | 20 +++++++------- .../json/providers/jackson3/Jackson3Node.java | 10 +++---- 6 files changed, 45 insertions(+), 47 deletions(-) rename lib/src/main/java/dev/harrel/jsonschema/internal/{GenericNode.java => StandaloneNode.java} (78%) diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java index d50fb8c4..7bc5e034 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java @@ -12,7 +12,7 @@ public final class InternalProviderUtil { private InternalProviderUtil() {} public static boolean canUseNativeEquals(JsonNode node) { - return node instanceof GenericNode || node instanceof AbstractJsonNode; + return node instanceof StandaloneNode || node instanceof AbstractJsonNode; } public static boolean canConvertToInteger(BigDecimal bigDecimal) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java similarity index 78% rename from lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java rename to lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java index 27c868e4..04906098 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/GenericNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java @@ -13,13 +13,13 @@ * Internal base class for all JSON provider implementations. * Not part of the contract and not intended for external use. */ -public final class GenericNode implements JsonNode { +public final class StandaloneNode implements JsonNode { private final String jsonPointer; private final SimpleType type; private final Object value; private Object altNumber; - public GenericNode(String jsonPointer, SimpleType type, Object value) { + public StandaloneNode(String jsonPointer, SimpleType type, Object value) { this.jsonPointer = Objects.requireNonNull(jsonPointer); this.type = Objects.requireNonNull(type); this.value = value; @@ -81,10 +81,10 @@ public Map asObject() { @Override public boolean equals(Object o) { - if (!(o instanceof GenericNode)) { + if (!(o instanceof StandaloneNode)) { return false; } - GenericNode that = (GenericNode) o; + StandaloneNode that = (StandaloneNode) o; return Objects.equals(value, that.value); } @@ -94,23 +94,23 @@ public int hashCode() { } @SuppressWarnings("unchecked") - public GenericNode copy(String jsonPointer) { + public StandaloneNode copy(String jsonPointer) { if (isArray()) { - List li = (List) value; - List copy = new ArrayList<>(li.size()); + List li = (List) value; + List copy = new ArrayList<>(li.size()); for (int i = 0; i < li.size(); i++) { copy.add(li.get(i).copy(jsonPointer + "/" + i)); } - return new GenericNode(jsonPointer, type, copy); + return new StandaloneNode(jsonPointer, type, copy); } else if (isObject()) { - Map map = (Map) value; - Map copy = newHashMap(map.size()); - for (Map.Entry entry : map.entrySet()) { + Map map = (Map) value; + Map copy = newHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { copy.put(entry.getKey(), entry.getValue().copy(jsonPointer + "/" + JsonNode.encodeJsonPointer(entry.getKey()))); } - return new GenericNode(jsonPointer, type, copy); + return new StandaloneNode(jsonPointer, type, copy); } else { - return new GenericNode(jsonPointer, type, value); + return new StandaloneNode(jsonPointer, type, value); } } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java index df6031dd..70e539fe 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -11,7 +11,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.SimpleType; -import dev.harrel.jsonschema.internal.GenericNode; +import dev.harrel.jsonschema.internal.StandaloneNode; import java.io.IOException; import java.math.BigDecimal; @@ -38,20 +38,20 @@ public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOE @Override @SuppressWarnings("deprecation") public JsonNode getNullValue() { - return new GenericNode("", SimpleType.NULL, null); + return new StandaloneNode("", SimpleType.NULL, null); } private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { switch (p.getCurrentToken()) { case VALUE_NULL: - return new GenericNode(jsonPointer, SimpleType.NULL, null); + return new StandaloneNode(jsonPointer, SimpleType.NULL, null); case VALUE_TRUE: case VALUE_FALSE: - return new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); case VALUE_STRING: - return new GenericNode(jsonPointer, SimpleType.STRING, p.getText()); + return new StandaloneNode(jsonPointer, SimpleType.STRING, p.getText()); case VALUE_NUMBER_INT: - return new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); case VALUE_NUMBER_FLOAT: return readNumber(p, jsonPointer); case START_ARRAY: @@ -71,9 +71,9 @@ private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { BigDecimal val = p.getDecimalValue(); if (canConvertToInteger(val)) { - return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); } else { - return new GenericNode(jsonPointer, SimpleType.NUMBER, val); + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); } } @@ -82,7 +82,7 @@ private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException while (p.nextToken() != JsonToken.END_ARRAY) { arr.add(readNode(p, jsonPointer + "/" + arr.size())); } - return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); } /* Using deprecated API to support older versions as well */ @@ -94,7 +94,7 @@ private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException p.nextToken(); obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); } - return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 71785805..8c6b5ce6 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -1,13 +1,11 @@ package dev.harrel.jsonschema.providers; -import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.module.SimpleModule; import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; -import dev.harrel.jsonschema.internal.GenericNode; +import dev.harrel.jsonschema.internal.StandaloneNode; import java.io.IOException; import java.math.BigDecimal; @@ -90,12 +88,12 @@ public JsonNode wrap(Object node) { if (node instanceof JacksonNode) { JacksonNode providerNode = (JacksonNode) node; return providerNode.jsonPointer.isEmpty() ? providerNode : new JacksonNode((providerNode).node); - } else if (node instanceof GenericNode) { - GenericNode genericNode = (GenericNode) node; - if (genericNode.getJsonPointer().isEmpty()) { - return genericNode; + } else if (node instanceof StandaloneNode) { + StandaloneNode standaloneNode = (StandaloneNode) node; + if (standaloneNode.getJsonPointer().isEmpty()) { + return standaloneNode; } else { - return genericNode.copy(""); + return standaloneNode.copy(""); } } else if (node instanceof com.fasterxml.jackson.databind.JsonNode) { return new JacksonNode((com.fasterxml.jackson.databind.JsonNode) node); diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java index 09fe7da7..0c0b145f 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java @@ -2,7 +2,7 @@ import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.SimpleType; -import dev.harrel.jsonschema.internal.GenericNode; +import dev.harrel.jsonschema.internal.StandaloneNode; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; @@ -23,15 +23,15 @@ public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws Jac @Override public JsonNode getNullValue(DeserializationContext ctx) { - return new GenericNode("", SimpleType.NULL, null); + return new StandaloneNode("", SimpleType.NULL, null); } private JsonNode readNode(JsonParser p, String jsonPointer) { return switch (p.currentToken()) { - case VALUE_NULL -> new GenericNode(jsonPointer, SimpleType.NULL, null); - case VALUE_TRUE, VALUE_FALSE -> new GenericNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); - case VALUE_STRING -> new GenericNode(jsonPointer, SimpleType.STRING, p.getString()); - case VALUE_NUMBER_INT -> new GenericNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NULL -> new StandaloneNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE, VALUE_FALSE -> new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING -> new StandaloneNode(jsonPointer, SimpleType.STRING, p.getString()); + case VALUE_NUMBER_INT -> new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); case VALUE_NUMBER_FLOAT -> readNumber(p, jsonPointer); case START_ARRAY -> readArray(p, jsonPointer); case START_OBJECT -> readObject(p, jsonPointer); @@ -47,9 +47,9 @@ private JsonNode readNumber(JsonParser p, String jsonPointer) { BigDecimal val = p.getDecimalValue(); // todo reuse if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { - return new GenericNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); } else { - return new GenericNode(jsonPointer, SimpleType.NUMBER, val); + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); } } @@ -58,7 +58,7 @@ private JsonNode readArray(JsonParser p, String jsonPointer) { while (p.nextToken() != JsonToken.END_ARRAY) { arr.add(readNode(p, jsonPointer + "/" + arr.size())); } - return new GenericNode(jsonPointer, SimpleType.ARRAY, arr); + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); } private JsonNode readObject(JsonParser p, String jsonPointer) { @@ -68,6 +68,6 @@ private JsonNode readObject(JsonParser p, String jsonPointer) { p.nextToken(); obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); } - return new GenericNode(jsonPointer, SimpleType.OBJECT, obj); + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); } } diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index 87401503..da041bd1 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -4,7 +4,7 @@ import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; -import dev.harrel.jsonschema.internal.GenericNode; +import dev.harrel.jsonschema.internal.StandaloneNode; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; @@ -86,11 +86,11 @@ public Factory(ObjectMapper mapper) { @Override public JsonNode wrap(Object node) { // todo support for old nodes as well - if (node instanceof GenericNode genericNode) { - if (genericNode.getJsonPointer().isEmpty()) { - return genericNode; + if (node instanceof StandaloneNode standaloneNode) { + if (standaloneNode.getJsonPointer().isEmpty()) { + return standaloneNode; } else { - return genericNode.copy(""); + return standaloneNode.copy(""); } } else if (node instanceof tools.jackson.databind.JsonNode providerNode) { return new Jackson3Node(providerNode); From a232218d0b08e576cdb58bd0edb88fc00c5a3f08 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 17:22:21 +0200 Subject: [PATCH 15/26] revert factory changes --- .../jsonschema/providers/JacksonNode.java | 17 +++++++-------- .../json/providers/jackson3/Jackson3Node.java | 21 +++++++------------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 8c6b5ce6..4929ac0c 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -75,7 +75,9 @@ public static final class Factory implements JsonNodeFactory { private final ObjectMapper mapper; public Factory() { - this(new ObjectMapper()); + this.mapper = new ObjectMapper() + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .registerModule(new JacksonModule()); } @Deprecated // todo since and doc, (should it be really deprecated?) @@ -84,17 +86,14 @@ public Factory(ObjectMapper mapper) { } @Override - public JsonNode wrap(Object node) { + public JacksonNode wrap(Object node) { if (node instanceof JacksonNode) { JacksonNode providerNode = (JacksonNode) node; return providerNode.jsonPointer.isEmpty() ? providerNode : new JacksonNode((providerNode).node); } else if (node instanceof StandaloneNode) { StandaloneNode standaloneNode = (StandaloneNode) node; - if (standaloneNode.getJsonPointer().isEmpty()) { - return standaloneNode; - } else { - return standaloneNode.copy(""); - } + return new JacksonNode(mapper.convertValue(standaloneNode, + com.fasterxml.jackson.databind.JsonNode.class)); } else if (node instanceof com.fasterxml.jackson.databind.JsonNode) { return new JacksonNode((com.fasterxml.jackson.databind.JsonNode) node); } else { @@ -103,9 +102,9 @@ public JsonNode wrap(Object node) { } @Override - public JsonNode create(String rawJson) { + public JacksonNode create(String rawJson) { try { - return mapper.readValue(rawJson, JsonNode.class); + return new JacksonNode(mapper.readTree(rawJson)); } catch (IOException e) { throw new IllegalArgumentException(e); } diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index da041bd1..4701fada 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -71,27 +71,20 @@ protected SimpleType computeNodeType(tools.jackson.databind.JsonNode node) { } public static final class Factory implements JsonNodeFactory { - private final JsonMapper mapper; + private final ObjectMapper mapper; public Factory() { this(JsonMapper.builder().enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS).build()); } public Factory(ObjectMapper mapper) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(JsonNode.class, new Jackson3Deserializer()); - this.mapper = JsonMapper.builder().addModule(module).build(); + this.mapper = mapper; } @Override - public JsonNode wrap(Object node) { - // todo support for old nodes as well - if (node instanceof StandaloneNode standaloneNode) { - if (standaloneNode.getJsonPointer().isEmpty()) { - return standaloneNode; - } else { - return standaloneNode.copy(""); - } + public Jackson3Node wrap(Object node) { + if (node instanceof Jackson3Node providerNode) { + return providerNode.jsonPointer.isEmpty() ? providerNode : new Jackson3Node(providerNode.node); } else if (node instanceof tools.jackson.databind.JsonNode providerNode) { return new Jackson3Node(providerNode); } else { @@ -100,8 +93,8 @@ public JsonNode wrap(Object node) { } @Override - public JsonNode create(String rawJson) { - return mapper.readValue(rawJson, JsonNode.class); + public Jackson3Node create(String rawJson) { + return new Jackson3Node(mapper.readTree(rawJson)); } } } From 53448ab345eb1a0576362da7c3a74466be7e945c Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 22:43:27 +0200 Subject: [PATCH 16/26] bring back lambda --- lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java index a7ddeaa7..d5b7384c 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java +++ b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java @@ -17,7 +17,7 @@ public final class ValidatorFactory { private final Map dialects = new HashMap<>(Dialects.OFFICIAL_DIALECTS); private Dialect defaultDialect = new Dialects.Draft2020Dialect(); private EvaluatorFactory evaluatorFactory; - private Supplier schemaNodeFactory = JacksonNode.Factory::new; + private Supplier schemaNodeFactory = () -> new JacksonNode.Factory(); // cannot be a method reference due to eager class loading private Supplier instanceNodeFactory = schemaNodeFactory; private SchemaResolver schemaResolver = new DefaultSchemaResolver(); private MessageProvider messageProvider = MessageProvider.fromLocale(Locale.getDefault()); From 804b69cda478add5839757288984c152b7db6d30 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 22:43:40 +0200 Subject: [PATCH 17/26] jackson standalone node test --- .../JacksonStandaloneNodeTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java diff --git a/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java b/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java new file mode 100644 index 00000000..d73e99ea --- /dev/null +++ b/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java @@ -0,0 +1,94 @@ +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.harrel.jsonschema.JsonNodeFactory; +import dev.harrel.jsonschema.ProviderTestBundle; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.ValidatorFactory; +import dev.harrel.jsonschema.internal.StandaloneNode; +import dev.harrel.jsonschema.providers.GsonNode; +import dev.harrel.jsonschema.providers.JacksonModule; +import dev.harrel.jsonschema.providers.JacksonNode; +import dev.harrel.jsonschema.util.JsonNodeMock; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JacksonStandaloneNodeTest extends ProviderTestBundle { + // factory to be promoted to prod code someday + private final JsonNodeFactory standaloneFactory = new JsonNodeFactory() { + private final ObjectMapper mapper = new ObjectMapper().registerModule(new JacksonModule()); + + @Override + public dev.harrel.jsonschema.JsonNode wrap(Object node) { + return switch (node) { + case StandaloneNode sNode when sNode.getJsonPointer().isEmpty() -> sNode; + case StandaloneNode sNode -> sNode.copy(""); + case dev.harrel.jsonschema.JsonNode otherNode -> mapper.convertValue(otherNode, dev.harrel.jsonschema.JsonNode.class); + case JsonNode providerNode -> mapper.convertValue(providerNode, dev.harrel.jsonschema.JsonNode.class); + default -> throw new IllegalArgumentException("Cannot wrap an instance of " + node.getClass().getName()); + }; + } + + @Override + public dev.harrel.jsonschema.JsonNode create(String rawJson) { + try { + return mapper.readValue(rawJson, dev.harrel.jsonschema.JsonNode.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }; + + @Override + public JsonNodeFactory getJsonNodeFactory() { + return standaloneFactory; + } + + @Test + void shouldInstantiateValidatorFactory() { + new ValidatorFactory(); + } + + @Test + void shouldPassForJacksonFactory() { + new ValidatorFactory() + .withJsonNodeFactory(new JacksonNode.Factory()) + .validate("{}", "{}"); + } + + @Test + void shouldPassForDefaultFactory() { + new ValidatorFactory().validate("{}", "{}"); + } + + @Test + void shouldFailForGsonFactory() { + AssertionsForClassTypes.assertThatThrownBy( + () -> new ValidatorFactory() + .withJsonNodeFactory(new GsonNode.Factory()) + .validate("{}", "{}")) + .isInstanceOf(NoClassDefFoundError.class); + } + + @Test + void shouldWrapForValidArgument() throws JsonProcessingException { + JsonNode object = new ObjectMapper().readTree("{}"); + dev.harrel.jsonschema.JsonNode wrap = new JacksonNode.Factory().wrap(object); + assertThat(wrap).isNotNull(); + assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); + } + + @Test + void shouldFailWrapForInvalidArgument() { + dev.harrel.jsonschema.JsonNode node = new JsonNodeMock(); + JacksonNode.Factory factory = new JacksonNode.Factory(); + assertThatThrownBy(() -> factory.wrap(node)) + .isInstanceOf(IllegalArgumentException.class); + } +} From 41834fb48f1c639a30b2f1dd77130b39c658778a Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 4 Jul 2026 22:45:18 +0200 Subject: [PATCH 18/26] expose serde --- .../java/dev/harrel/jsonschema/providers/JacksonModule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java index 70e539fe..52aaa358 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -28,7 +28,7 @@ public JacksonModule() { Collections.singletonList(new Serializer())); } - static final class Deserializer extends JsonDeserializer { + public static final class Deserializer extends JsonDeserializer { @Override public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { return readNode(p, ""); @@ -98,7 +98,7 @@ private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException } } - static final class Serializer extends JsonSerializer { + public static final class Serializer extends JsonSerializer { @Override public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { writeNode(value, gen); From b748d0077761de0095d34fe3597d74b2ca16d57e Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 5 Jul 2026 00:21:58 +0200 Subject: [PATCH 19/26] gson serde --- .../gsonTest/GsonStandaloneNodeTest.java | 80 ++++++++++++ .../jsonschema/internal/StandaloneNode.java | 5 + .../jsonschema/providers/GsonModule.java | 116 ++++++++++++++++++ .../jsonschema/providers/JacksonModule.java | 105 ++++++++-------- 4 files changed, 251 insertions(+), 55 deletions(-) create mode 100644 lib/src/integration/gsonTest/GsonStandaloneNodeTest.java create mode 100644 lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java diff --git a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java new file mode 100644 index 00000000..34f7de60 --- /dev/null +++ b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java @@ -0,0 +1,80 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import dev.harrel.jsonschema.*; +import dev.harrel.jsonschema.internal.StandaloneNode; +import dev.harrel.jsonschema.providers.GsonModule; +import dev.harrel.jsonschema.providers.GsonNode; +import dev.harrel.jsonschema.util.JsonNodeMock; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class GsonStandaloneNodeTest extends ProviderTestBundle { + // factory to be promoted to prod code someday + private final JsonNodeFactory standaloneFactory = new JsonNodeFactory() { + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(JsonNode.class, new GsonModule.TypeAdapter()) + .create(); + + @Override + public dev.harrel.jsonschema.JsonNode wrap(Object node) { + return switch (node) { + case StandaloneNode sNode when sNode.getJsonPointer().isEmpty() -> sNode; + case StandaloneNode sNode -> sNode.copy(""); + + // todo - try using adapter directly to omit String serialization + case JsonNode otherNode -> gson.fromJson(gson.toJson(otherNode), JsonNode.class); + case JsonElement providerNode -> gson.fromJson(gson.toJson(providerNode), JsonNode.class); + default -> + throw new IllegalArgumentException("Cannot wrap an instance of " + node.getClass().getName()); + }; + } + + @Override + public dev.harrel.jsonschema.JsonNode create(String rawJson) { + return gson.fromJson(rawJson, JsonNode.class); + } + }; + + @Override + public JsonNodeFactory getJsonNodeFactory() { + return new GsonNode.Factory(); + } + + @Test + void shouldInstantiateValidatorFactory() { + new ValidatorFactory(); + } + + @Test + void shouldPassForGsonFactory() { + new ValidatorFactory() + .withJsonNodeFactory(new GsonNode.Factory()) + .validate("{}", "{}"); + } + + @Test + void shouldFailForDefaultFactory() { + assertThatThrownBy(() -> new ValidatorFactory().validate("{}", "{}")) + .isInstanceOf(NoClassDefFoundError.class); + } + + @Test + void shouldWrapForValidArgument() { + JsonElement object = new JsonParser().parse("{}"); + JsonNode wrap = new GsonNode.Factory().wrap(object); + assertThat(wrap).isNotNull(); + assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); + } + + @Test + void shouldFailWrapForInvalidArgument() { + JsonNode node = new JsonNodeMock(); + JsonNodeFactory factory = getJsonNodeFactory(); + assertThatThrownBy(() -> factory.wrap(node)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java index 04906098..6e75fcf9 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java @@ -20,9 +20,14 @@ public final class StandaloneNode implements JsonNode { private Object altNumber; public StandaloneNode(String jsonPointer, SimpleType type, Object value) { + this(jsonPointer, type, value, null); + } + + public StandaloneNode(String jsonPointer, SimpleType type, Object value, Object altNumber) { this.jsonPointer = Objects.requireNonNull(jsonPointer); this.type = Objects.requireNonNull(type); this.value = value; + this.altNumber = altNumber; } @Override diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java new file mode 100644 index 00000000..1ff75ed7 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java @@ -0,0 +1,116 @@ +package dev.harrel.jsonschema.providers; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.StandaloneNode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + +// todo doc +public final class GsonModule { + private GsonModule() {} + + public static class TypeAdapter extends com.google.gson.TypeAdapter { + @Override + public void write(JsonWriter out, JsonNode value) throws IOException { + writeNode(out, value); + } + + private void writeNode(JsonWriter out, JsonNode value) throws IOException { + switch (value.getNodeType()) { + case NULL: + out.nullValue(); + return; + case BOOLEAN: + out.value(value.asBoolean()); + return; + case STRING: + out.value(value.asString()); + return; + case INTEGER: + out.value(value.asInteger()); + return; + case NUMBER: + out.value(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + out.beginArray(); + for (int i = 0; i < arr.size(); i++) { + writeNode(out, arr.get(i)); + } + out.endArray(); + return; + case OBJECT: + Map map = value.asObject(); + out.beginObject(); + for (Map.Entry entry : map.entrySet()) { + out.name(entry.getKey()); + writeNode(out, entry.getValue()); + } + out.endObject(); + } + } + + @Override + public JsonNode read(JsonReader in) throws IOException { + return readNode(in, ""); + } + + private JsonNode readNode(JsonReader in, String jsonPointer) throws IOException { + switch (in.peek()) { + case NULL: + in.nextNull(); + return new StandaloneNode(jsonPointer, SimpleType.NULL, null); + case BOOLEAN: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, in.nextBoolean()); + case STRING: + return new StandaloneNode(jsonPointer, SimpleType.STRING, in.nextString()); + case NUMBER: + BigDecimal val = new BigDecimal(in.nextString()); + if (canConvertToInteger(val)) { + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger(), val); + } else { + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); + } + case BEGIN_ARRAY: + return readArray(in, jsonPointer); + case BEGIN_OBJECT: + return readObject(in, jsonPointer); + default: + throw new IllegalArgumentException("Unexpected token: " + in.peek().name()); + } + } + + private JsonNode readArray(JsonReader in, String jsonPointer) throws IOException { + in.beginArray(); + List arr = new ArrayList<>(); + while (in.peek() != JsonToken.END_ARRAY) { + arr.add(readNode(in, jsonPointer + "/" + arr.size())); + } + in.endArray(); + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); + } + + private JsonNode readObject(JsonReader in, String jsonPointer) throws IOException { + in.beginObject(); + Map obj = new LinkedHashMap<>(); + while (in.peek() != JsonToken.END_OBJECT) { + String name = in.nextName(); + obj.put(name, readNode(in, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + in.endObject(); + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); + } + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java index 52aaa358..d5866784 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -28,6 +28,54 @@ public JacksonModule() { Collections.singletonList(new Serializer())); } + public static final class Serializer extends JsonSerializer { + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + writeNode(value, gen); + } + + @Override + public Class handledType() { + return JsonNode.class; + } + + private void writeNode(JsonNode value, JsonGenerator gen) throws IOException { + switch (value.getNodeType()) { + case NULL: + gen.writeNull(); + return; + case BOOLEAN: + gen.writeBoolean(value.asBoolean()); + return; + case STRING: + gen.writeString(value.asString()); + return; + case INTEGER: + gen.writeNumber(value.asInteger()); + return; + case NUMBER: + gen.writeNumber(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + gen.writeStartArray(null, arr.size()); + for (int i = 0; i < arr.size(); i++) { + writeNode(arr.get(i), gen); + } + gen.writeEndArray(); + return; + case OBJECT: + Map map = value.asObject(); + gen.writeStartObject(null, map.size()); + for (Map.Entry entry : map.entrySet()) { + gen.writeFieldName(entry.getKey()); + writeNode(entry.getValue(), gen); + } + gen.writeEndObject(); + } + } + } + public static final class Deserializer extends JsonDeserializer { @Override public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { @@ -58,20 +106,15 @@ private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { return readArray(p, jsonPointer); case START_OBJECT: return readObject(p, jsonPointer); - case NOT_AVAILABLE: - case END_OBJECT: - case END_ARRAY: - case FIELD_NAME: - case VALUE_EMBEDDED_OBJECT: default: - throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg + throw new IllegalArgumentException("Unexpected token: " + p.currentToken().name()); } } private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { BigDecimal val = p.getDecimalValue(); if (canConvertToInteger(val)) { - return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger(), val); } else { return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); } @@ -97,52 +140,4 @@ private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); } } - - public static final class Serializer extends JsonSerializer { - @Override - public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - writeNode(value, gen); - } - - @Override - public Class handledType() { - return JsonNode.class; - } - - void writeNode(JsonNode value, JsonGenerator gen) throws IOException { - switch (value.getNodeType()) { - case NULL: - gen.writeNull(); - return; - case BOOLEAN: - gen.writeBoolean(value.asBoolean()); - return; - case STRING: - gen.writeString(value.asString()); - return; - case INTEGER: - gen.writeNumber(value.asInteger()); - return; - case NUMBER: - gen.writeNumber(value.asNumber()); - return; - case ARRAY: - List arr = value.asArray(); - gen.writeStartArray(null, arr.size()); - for (int i = 0; i < arr.size(); i++) { - writeNode(arr.get(i), gen); - } - gen.writeEndArray(); - return; - case OBJECT: - Map map = value.asObject(); - gen.writeStartObject(null, map.size()); - for (Map.Entry entry : map.entrySet()) { - gen.writeFieldName(entry.getKey()); - writeNode(entry.getValue(), gen); - } - gen.writeEndObject(); - } - } - } } From 1afcb0d166c5e19823c9c391a766294cf0fe89dc Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 5 Jul 2026 00:27:13 +0200 Subject: [PATCH 20/26] actually use new code... --- lib/src/integration/gsonTest/GsonStandaloneNodeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java index 34f7de60..83737698 100644 --- a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java +++ b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java @@ -41,7 +41,7 @@ public dev.harrel.jsonschema.JsonNode create(String rawJson) { @Override public JsonNodeFactory getJsonNodeFactory() { - return new GsonNode.Factory(); + return standaloneFactory; } @Test From f331b7e2c8feec454f9ad69ee891576adacf5715 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 5 Jul 2026 11:48:25 +0200 Subject: [PATCH 21/26] adjust tests --- .../gsonTest/GsonStandaloneNodeTest.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java index 83737698..a1f40135 100644 --- a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java +++ b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java @@ -1,7 +1,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; -import com.google.gson.JsonParser; import dev.harrel.jsonschema.*; import dev.harrel.jsonschema.internal.StandaloneNode; import dev.harrel.jsonschema.providers.GsonModule; @@ -9,6 +8,8 @@ import dev.harrel.jsonschema.util.JsonNodeMock; import org.junit.jupiter.api.Test; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -16,7 +17,7 @@ class GsonStandaloneNodeTest extends ProviderTestBundle { // factory to be promoted to prod code someday private final JsonNodeFactory standaloneFactory = new JsonNodeFactory() { private final Gson gson = new GsonBuilder() - .registerTypeAdapter(JsonNode.class, new GsonModule.TypeAdapter()) + .registerTypeHierarchyAdapter(JsonNode.class, new GsonModule.TypeAdapter()) .create(); @Override @@ -24,8 +25,6 @@ public dev.harrel.jsonschema.JsonNode wrap(Object node) { return switch (node) { case StandaloneNode sNode when sNode.getJsonPointer().isEmpty() -> sNode; case StandaloneNode sNode -> sNode.copy(""); - - // todo - try using adapter directly to omit String serialization case JsonNode otherNode -> gson.fromJson(gson.toJson(otherNode), JsonNode.class); case JsonElement providerNode -> gson.fromJson(gson.toJson(providerNode), JsonNode.class); default -> @@ -64,17 +63,17 @@ void shouldFailForDefaultFactory() { @Test void shouldWrapForValidArgument() { - JsonElement object = new JsonParser().parse("{}"); + JsonElement object = new Gson().toJsonTree(Map.of()); JsonNode wrap = new GsonNode.Factory().wrap(object); assertThat(wrap).isNotNull(); assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); } @Test - void shouldFailWrapForInvalidArgument() { - JsonNode node = new JsonNodeMock(); + void shouldWrapRudimentaryJsonNodeImpl() { JsonNodeFactory factory = getJsonNodeFactory(); - assertThatThrownBy(() -> factory.wrap(node)) - .isInstanceOf(IllegalArgumentException.class); + JsonNode node = factory.wrap(new JsonNodeMock()); + assertThat(node.isBoolean()).isTrue(); + assertThat(node.asBoolean()).isFalse(); } } From d67d06eebdada9619b9bea89805d336c268912d9 Mon Sep 17 00:00:00 2001 From: harrel Date: Sun, 5 Jul 2026 14:07:32 +0200 Subject: [PATCH 22/26] get rid of StringNode, fix native equals check --- .../dev/harrel/jsonschema/Applicators.java | 4 +- .../dev/harrel/jsonschema/Evaluators.java | 10 ++-- .../dev/harrel/jsonschema/StringNode.java | 59 ------------------- .../internal/InternalProviderUtil.java | 6 +- .../dev/harrel/jsonschema/JsonNodeTest.java | 3 +- 5 files changed, 14 insertions(+), 68 deletions(-) delete mode 100644 lib/src/main/java/dev/harrel/jsonschema/StringNode.java diff --git a/lib/src/main/java/dev/harrel/jsonschema/Applicators.java b/lib/src/main/java/dev/harrel/jsonschema/Applicators.java index 039326d2..5005cc04 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/Applicators.java +++ b/lib/src/main/java/dev/harrel/jsonschema/Applicators.java @@ -1,5 +1,7 @@ package dev.harrel.jsonschema; +import dev.harrel.jsonschema.internal.StandaloneNode; + import java.math.BigInteger; import java.util.*; import java.util.regex.Pattern; @@ -358,7 +360,7 @@ public Result evaluate(EvaluationContext ctx, JsonNode node) { boolean valid = true; for (String propName : node.asObject().keySet()) { - valid = ctx.resolveInternalRefAndValidate(schemaRef, new StringNode(propName, node.getJsonPointer())) && valid; + valid = ctx.resolveInternalRefAndValidate(schemaRef, new StandaloneNode(node.getJsonPointer(), SimpleType.STRING, propName)) && valid; } return valid ? Result.success() : Result.failure(); } diff --git a/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java b/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java index e0c61fa4..51e68be9 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java +++ b/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java @@ -48,26 +48,26 @@ class ConstEvaluator implements Evaluator { @Override public Result evaluate(EvaluationContext ctx, JsonNode node) { - boolean valid = canUseNativeEquals(constNode) && canUseNativeEquals(node) ? constNode.equals(node) : JsonNodeUtil.equals(constNode, node); + boolean valid = canUseNativeEquals(constNode, node) ? constNode.equals(node) : JsonNodeUtil.equals(constNode, node); return valid ? Result.success() : Result.formattedFailure("const", node.toPrintableString(), constNode.toPrintableString()); } } class EnumEvaluator implements Evaluator { + private final JsonNode arrayNode; private final Set enumNodes; - private final boolean canUseNativeEquals; EnumEvaluator(JsonNode node) { if (!node.isArray()) { throw new IllegalArgumentException(); } + this.arrayNode = node; this.enumNodes = unmodifiableSet(new LinkedHashSet<>(node.asArray())); - this.canUseNativeEquals = canUseNativeEquals(node); } @Override public Result evaluate(EvaluationContext ctx, JsonNode node) { - if (canUseNativeEquals && canUseNativeEquals(node)) { + if (canUseNativeEquals(arrayNode, node)) { return enumNodes.contains(node) ? Result.success() : Result.formattedFailure("enum", createArgsSupplier(node)); } else { for (JsonNode enumNode : enumNodes) { @@ -387,7 +387,7 @@ public Result evaluate(EvaluationContext ctx, JsonNode node) { } List jsonNodes = node.asArray(); - if (canUseNativeEquals(node)) { + if (canUseNativeEquals(node, node)) { Set parsed = new HashSet<>(); for (int i = 0; i < jsonNodes.size(); i++) { if (!parsed.add(jsonNodes.get(i))) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/StringNode.java b/lib/src/main/java/dev/harrel/jsonschema/StringNode.java deleted file mode 100644 index 55216417..00000000 --- a/lib/src/main/java/dev/harrel/jsonschema/StringNode.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.harrel.jsonschema; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; - -final class StringNode implements JsonNode { - private final String value; - private final String jsonPointer; - - public StringNode(String value, String jsonPointer) { - this.value = value; - this.jsonPointer = jsonPointer; - } - - @Override - public SimpleType getNodeType() { - return SimpleType.STRING; - } - - @Override - public String getJsonPointer() { - return jsonPointer; - } - - @Override - public boolean asBoolean() { - return false; - } - - @Override - public String asString() { - return value; - } - - @Override - public BigInteger asInteger() { - return null; - } - - @Override - public BigDecimal asNumber() { - return null; - } - - @Override - public List asArray() { - return emptyList(); - } - - @Override - public Map asObject() { - return emptyMap(); - } -} diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java index 7bc5e034..b8885a86 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java @@ -1,3 +1,4 @@ + package dev.harrel.jsonschema.internal; import dev.harrel.jsonschema.JsonNode; @@ -11,8 +12,9 @@ public final class InternalProviderUtil { private InternalProviderUtil() {} - public static boolean canUseNativeEquals(JsonNode node) { - return node instanceof StandaloneNode || node instanceof AbstractJsonNode; + public static boolean canUseNativeEquals(JsonNode n1, JsonNode n2) { + return n1 instanceof StandaloneNode && n2 instanceof StandaloneNode || + n1 instanceof AbstractJsonNode && n2 instanceof AbstractJsonNode; } public static boolean canConvertToInteger(BigDecimal bigDecimal) { diff --git a/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java b/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java index 6e3d6289..0213f129 100644 --- a/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java +++ b/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java @@ -1,5 +1,6 @@ package dev.harrel.jsonschema; +import dev.harrel.jsonschema.util.JsonNodeMock; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -365,7 +366,7 @@ void objectEquals() { void isNotEqualToDifferentType() { JsonNodeFactory nodeFactory = getJsonNodeFactory(); JsonNode node = nodeFactory.create("{}"); - assertThat(node.equals(new StringNode("hello?", ""))).isFalse(); + assertThat(node.equals(new JsonNodeMock())).isFalse(); assertThat(node.equals(new Object())).isFalse(); } } From 6e97753150fd9a779bf7bd6c215be117651f3ed3 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 11 Jul 2026 20:57:22 +0200 Subject: [PATCH 23/26] use token id in jackson serde --- .../jsonschema/providers/JacksonModule.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java index d5866784..ad4806e8 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -1,9 +1,6 @@ package dev.harrel.jsonschema.providers; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -17,6 +14,7 @@ import java.math.BigDecimal; import java.util.*; +import static com.fasterxml.jackson.core.JsonTokenId.*; import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; // todo doc @@ -90,21 +88,22 @@ public JsonNode getNullValue() { } private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { - switch (p.getCurrentToken()) { - case VALUE_NULL: + switch (p.currentTokenId()) { + case ID_NULL: return new StandaloneNode(jsonPointer, SimpleType.NULL, null); - case VALUE_TRUE: - case VALUE_FALSE: - return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); - case VALUE_STRING: + case ID_TRUE: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, Boolean.TRUE); + case ID_FALSE: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, Boolean.FALSE); + case ID_STRING: return new StandaloneNode(jsonPointer, SimpleType.STRING, p.getText()); - case VALUE_NUMBER_INT: + case ID_NUMBER_INT: return new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); - case VALUE_NUMBER_FLOAT: + case ID_NUMBER_FLOAT: return readNumber(p, jsonPointer); - case START_ARRAY: + case ID_START_ARRAY: return readArray(p, jsonPointer); - case START_OBJECT: + case ID_START_OBJECT: return readObject(p, jsonPointer); default: throw new IllegalArgumentException("Unexpected token: " + p.currentToken().name()); From 467a1d218d8f5b72fa461c000cf6a4fc79cdfd4e Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 11 Jul 2026 21:42:09 +0200 Subject: [PATCH 24/26] introduce JsonNodeFactoryWrapper --- .../jsonschema/JsonNodeFactoryWrapper.java | 25 +++++++++++++++++++ .../harrel/jsonschema/ValidatorFactory.java | 4 +-- .../jsonschema/providers/JacksonNode.java | 4 --- 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java diff --git a/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java b/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java new file mode 100644 index 00000000..5c88ed81 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java @@ -0,0 +1,25 @@ +package dev.harrel.jsonschema; + +import dev.harrel.jsonschema.internal.StandaloneNode; + +final class JsonNodeFactoryWrapper implements JsonNodeFactory { + private final JsonNodeFactory delegate; + + JsonNodeFactoryWrapper(JsonNodeFactory delegate) { + this.delegate = delegate; + } + + @Override + public JsonNode wrap(Object node) { + if (node instanceof StandaloneNode) { + StandaloneNode sNode = (StandaloneNode) node; + return sNode.getJsonPointer().isEmpty() ? sNode : sNode.copy(""); + } + return delegate.wrap(node); + } + + @Override + public JsonNode create(String rawJson) { + return delegate.create(rawJson); + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java index d5b7384c..ee278bc3 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java +++ b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java @@ -116,8 +116,8 @@ public ValidatorFactory withJsonNodeFactory(JsonNodeFactory jsonNodeFactory) { public ValidatorFactory withJsonNodeFactories(JsonNodeFactory schemaNodeFactory, JsonNodeFactory instanceNodeFactory) { Objects.requireNonNull(schemaNodeFactory); Objects.requireNonNull(instanceNodeFactory); - this.schemaNodeFactory = () -> schemaNodeFactory; - this.instanceNodeFactory = () -> instanceNodeFactory; + this.schemaNodeFactory = () -> new JsonNodeFactoryWrapper(schemaNodeFactory); + this.instanceNodeFactory = () -> new JsonNodeFactoryWrapper(instanceNodeFactory); return this; } diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 4929ac0c..4099d073 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -90,10 +90,6 @@ public JacksonNode wrap(Object node) { if (node instanceof JacksonNode) { JacksonNode providerNode = (JacksonNode) node; return providerNode.jsonPointer.isEmpty() ? providerNode : new JacksonNode((providerNode).node); - } else if (node instanceof StandaloneNode) { - StandaloneNode standaloneNode = (StandaloneNode) node; - return new JacksonNode(mapper.convertValue(standaloneNode, - com.fasterxml.jackson.databind.JsonNode.class)); } else if (node instanceof com.fasterxml.jackson.databind.JsonNode) { return new JacksonNode((com.fasterxml.jackson.databind.JsonNode) node); } else { From 8a2475bfbda104ee5258ffb308b76b1df3a24ecd Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 11 Jul 2026 21:48:33 +0200 Subject: [PATCH 25/26] Revert "use token id in jackson serde" This reverts commit 6e97753150fd9a779bf7bd6c215be117651f3ed3. --- .../jsonschema/providers/JacksonModule.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java index ad4806e8..d5866784 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -1,6 +1,9 @@ package dev.harrel.jsonschema.providers; -import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -14,7 +17,6 @@ import java.math.BigDecimal; import java.util.*; -import static com.fasterxml.jackson.core.JsonTokenId.*; import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; // todo doc @@ -88,22 +90,21 @@ public JsonNode getNullValue() { } private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { - switch (p.currentTokenId()) { - case ID_NULL: + switch (p.getCurrentToken()) { + case VALUE_NULL: return new StandaloneNode(jsonPointer, SimpleType.NULL, null); - case ID_TRUE: - return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, Boolean.TRUE); - case ID_FALSE: - return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, Boolean.FALSE); - case ID_STRING: + case VALUE_TRUE: + case VALUE_FALSE: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING: return new StandaloneNode(jsonPointer, SimpleType.STRING, p.getText()); - case ID_NUMBER_INT: + case VALUE_NUMBER_INT: return new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); - case ID_NUMBER_FLOAT: + case VALUE_NUMBER_FLOAT: return readNumber(p, jsonPointer); - case ID_START_ARRAY: + case START_ARRAY: return readArray(p, jsonPointer); - case ID_START_OBJECT: + case START_OBJECT: return readObject(p, jsonPointer); default: throw new IllegalArgumentException("Unexpected token: " + p.currentToken().name()); From 9989c29e933f0964af1df41735774d8cea1d1553 Mon Sep 17 00:00:00 2001 From: harrel Date: Sat, 11 Jul 2026 22:09:35 +0200 Subject: [PATCH 26/26] revert old factory fully --- .../dev/harrel/jsonschema/providers/JacksonNode.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 4099d073..6a7c4fec 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -5,7 +5,6 @@ import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; -import dev.harrel.jsonschema.internal.StandaloneNode; import java.io.IOException; import java.math.BigDecimal; @@ -71,18 +70,15 @@ protected SimpleType computeNodeType(com.fasterxml.jackson.databind.JsonNode nod } } - public static final class Factory implements JsonNodeFactory { + public static final class Factory implements JsonNodeFactory { private final ObjectMapper mapper; public Factory() { - this.mapper = new ObjectMapper() - .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) - .registerModule(new JacksonModule()); + this(new ObjectMapper().enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)); } - @Deprecated // todo since and doc, (should it be really deprecated?) public Factory(ObjectMapper mapper) { - this.mapper = mapper.copy().registerModule(new JacksonModule()); + this.mapper = mapper; } @Override