From 06c5bcbb710b60ad208eb8b67843a8cfb9104451 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Tue, 19 May 2026 17:26:23 -0700 Subject: [PATCH 01/10] Initial checkin of JRefValueDeserializer/JRefResolver --- .../tools/jackson/databind/JRefModule.java | 27 +++ .../databind/JRefResolveException.java | 32 +++ .../tools/jackson/databind/JRefResolver.java | 182 ++++++++++++++++++ .../databind/JRefValueDeserializer.java | 60 ++++++ .../tools/jackson/databind/ObjectMapper.java | 8 + .../databind/deser/impl/MethodProperty.java | 24 ++- .../deser/bean/JRefBeanDeserializerTest.java | 82 ++++++++ 7 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 src/main/java/tools/jackson/databind/JRefModule.java create mode 100644 src/main/java/tools/jackson/databind/JRefResolveException.java create mode 100644 src/main/java/tools/jackson/databind/JRefResolver.java create mode 100644 src/main/java/tools/jackson/databind/JRefValueDeserializer.java create mode 100644 src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java diff --git a/src/main/java/tools/jackson/databind/JRefModule.java b/src/main/java/tools/jackson/databind/JRefModule.java new file mode 100644 index 0000000000..103caa4cb6 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefModule.java @@ -0,0 +1,27 @@ +package tools.jackson.databind; + +import tools.jackson.databind.BeanDescription.Supplier; +import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.module.SimpleModule; + +public class JRefModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public JRefModule() { + super("JRef"); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.addDeserializerModifier(new ValueDeserializerModifier() { + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, Supplier beanDescRef, + ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + }); + } +} + diff --git a/src/main/java/tools/jackson/databind/JRefResolveException.java b/src/main/java/tools/jackson/databind/JRefResolveException.java new file mode 100644 index 0000000000..0380273b12 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefResolveException.java @@ -0,0 +1,32 @@ +package tools.jackson.databind; + +public class JRefResolveException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final JRefResolver resolver; + private final Object root; + + public JRefResolveException(JRefResolver resolver, Object root, String message, Exception cause) { + super(message, cause); + this.resolver = resolver; + this.root = root; + } + + public JRefResolveException(JRefResolver resolver, Object root, String message) { + super(message); + this.resolver = resolver; + this.root = root; + } + + public JRefResolveException(JRefResolver resolver, String message) { + this(resolver, null, message); + } + + public JRefResolver getResolver() { + return this.resolver; + } + + public Object getRoot() { + return this.root; + } +} diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java new file mode 100644 index 0000000000..ea5a0ade11 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -0,0 +1,182 @@ +package tools.jackson.databind; + +import java.lang.reflect.Field; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.deser.impl.MethodProperty; + +public class JRefResolver { + + private final DeserializationContext ctxt; + private final String path; + private MethodProperty methodProperty; + private Object targetInstance; + + public JRefResolver(DeserializationContext ctxt, String path) { + Objects.requireNonNull(ctxt, "deserialization context must not be null"); + this.ctxt = ctxt; + Objects.requireNonNull(path, "path must must not be null"); + this.path = path; + } + + public void setSetter(MethodProperty methodProperty, Object targetInstance) { + this.methodProperty = methodProperty; + this.targetInstance = targetInstance; + } + + public void resolve(Object root) throws JRefResolveException { + if (root == null) { + throw new JRefResolveException(this, "Root object cannot be null"); + } + if (this.methodProperty == null) { + throw new JRefResolveException(this, root, "methodProperty is null. methodProperty must be set prior to calling resolve"); + } + // Now that we have the root, we can lookup the object at path + Object value = resolvePathToValue(root); + try { + this.methodProperty.set(ctxt, targetInstance, value); + } catch (JacksonException e) { + throw new JRefResolveException(this, root, "Exception setting value", e); + } + } + + private static String decode_uri(String uri) { + try { + return URLDecoder.decode(uri, StandardCharsets.UTF_8.toString()); + } catch (Exception e) { + return uri; + } + } + + protected String unescape(String segment) { + return segment.replace("~1", "/").replace("~0", "~"); + } + + protected String escape(String segment) { + return segment.replace("~", "~0").replace("/", "~1"); + } + + protected Iterable pointerSegments(String pointer) { + if (pointer.length() > 0 && !pointer.startsWith("/")) { + throw new IllegalArgumentException("Invalid JSON Pointer"); + } + + List segments = new ArrayList<>(); + int segmentStart = 1; + int segmentEnd; + + while (segmentStart <= pointer.length()) { + int position = pointer.indexOf("/", segmentStart); + segmentEnd = (position == -1) ? pointer.length() : position; + String segment = pointer.substring(segmentStart, segmentEnd); + segmentStart = segmentEnd + 1; + + segments.add(unescape(segment)); + + // If the pointer ended with a '/', we need to add an empty segment for the + // trailing slash + if (position != -1 && segmentStart > pointer.length()) { + segments.add(""); + } + } + + return segments; + } + + protected Object computeSegment(Object value, String segment) { + if (value instanceof List) { + return "-".equals(segment) ? ((List) value).size() : Integer.parseInt(segment); + } else { + return segment; + } + } + + protected Object get(String pointer, Object subject) { + if (subject == null) { + final List segments = new ArrayList<>(); + pointerSegments(pointer).forEach(segments::add); + return (Function) (Object s) -> _get(segments, s); + } else { + return _get(pointerSegments(pointer), subject); + } + } + + protected Object applySegment(Object value, Object segment, String cursor) { + if (value == null) { + throw new RuntimeException(String.format("Value at '%s' is %s and does not have property '%s'", cursor, + (cursor.isEmpty() ? "null" : "undefined"), segment)); + } else { + Object computedSegment = computeSegment(value, String.valueOf(segment)); + if (value instanceof Map) { + Map map = (Map) value; + if (map.containsKey(computedSegment)) { + return map.get(computedSegment); + } + } else if (value instanceof List) { + List list = (List) value; + if (computedSegment instanceof Integer) { + int index = (Integer) computedSegment; + if (index >= 0 && index < list.size()) { + return list.get(index); + } + } + } + return getAccessibleFieldValue(value, String.valueOf(computedSegment)); + } + } + + protected Field getAccessibleField(Class clazz, String fieldName) { + try { + Field f = clazz.getDeclaredField(fieldName); + f.setAccessible(true); + return f; + } catch (NoSuchFieldException | SecurityException e) { + throw new RuntimeException(String.format("Could not find field on class=%s with name=%s", clazz, fieldName), + e); + } + } + + protected Object getAccessibleFieldValue(Object value, String fieldName) { + try { + return getAccessibleField(value.getClass(), fieldName).get(value); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException( + String.format("Could not get value for field=%s on object=%s with field", fieldName, value)); + } + } + + + protected Object _get(Iterable segments, Object subject) { + String cursor = ""; + for (String segment : segments) { + subject = applySegment(subject, segment, cursor); + cursor = append(segment, cursor); + } + return subject; + } + + protected String append(Object segment, String pointer) { + return pointer + "/" + escape(String.valueOf(segment)); + } + + protected Object resolvePathToValue(Object root) { + String refStr = this.path; + String[] parts = refStr.split("#", 2); + if (parts.length > 1) { + Object refValue = get(decode_uri(parts[1]), root); + if (refValue == null) { + throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.path + " not found on root=" + root); + } + return refValue; + } + throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.path + " does not have preceding '#'"); + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java new file mode 100644 index 0000000000..36352ddd91 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java @@ -0,0 +1,60 @@ +package tools.jackson.databind; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.TreeTraversingParser; + +public class JRefValueDeserializer extends DelegatingDeserializer { + + public static final String JREF = "$ref"; + + void trace(String method, JRefValueDeserializer vds) { + StringBuffer buf = new StringBuffer(method).append("."); + buf.append(vds.toString()); + System.out.println(buf.toString()); + } + + void trace(String method) { + trace(method, this); + } + + public JRefValueDeserializer(ValueDeserializer src) { + super(src); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + trace("deserialize"); + Object result = null; + JsonToken current = p.currentToken(); + if (current == JsonToken.START_OBJECT) { + JsonNode node = ctxt.readTree(p); + if (node instanceof ObjectNode) { + ObjectNode onode = (ObjectNode) node; + JsonNode valNode = onode.get(JREF); + if (valNode != null) { + String valStr = valNode.asString(); + if (valStr != null) { + return new JRefResolver(ctxt, valStr); + } + } + } + TreeTraversingParser tpp = new TreeTraversingParser(node); + tpp.nextToken(); + result = super.deserialize(tpp, ctxt); + } else { + result = super.deserialize(p, ctxt); + } + trace("deserialized result=" + result); + return result; + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new JRefValueDeserializer(newDelegatee); + } + +} diff --git a/src/main/java/tools/jackson/databind/ObjectMapper.java b/src/main/java/tools/jackson/databind/ObjectMapper.java index f1651c0444..f8f502ce32 100644 --- a/src/main/java/tools/jackson/databind/ObjectMapper.java +++ b/src/main/java/tools/jackson/databind/ObjectMapper.java @@ -2666,6 +2666,14 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, result = ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null); ctxt.checkUnresolvedObjectId(); + // XXX JREF resolve jrefs + @SuppressWarnings("unchecked") + List jrefs = (List) ctxt.getAttribute("jrefs"); + if (jrefs != null) { + jrefs.forEach(r -> { + r.resolve(result); + }); + } } if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, valueType); diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index 4d8f4b78c9..c2fe09dd53 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -4,6 +4,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; @@ -128,10 +130,24 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - try { - _setter.get().invokeExact(instance, value); - } catch (Throwable e) { - _throwAsJacksonE(p, e, value); + // XXX JREF handling + if (value instanceof JRefResolver) { + JRefResolver r = (JRefResolver) value; + // pass in setter + r.setSetter(this, instance); + @SuppressWarnings("unchecked") + List jrefs = (List) ctxt.getAttribute("jrefs"); + if (jrefs == null) { + jrefs = new ArrayList(); + ctxt.setAttribute("jrefs", jrefs); + } + jrefs.add(r); + } else { + try { + _setter.get().invokeExact(instance, value); + } catch (Throwable e) { + _throwAsJacksonE(p, e, value); + } } } diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java new file mode 100644 index 0000000000..396b110317 --- /dev/null +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -0,0 +1,82 @@ +package tools.jackson.databind.deser.bean; + +import static tools.jackson.databind.testutil.DatabindTestUtil.jsonMapperBuilder; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.JRefModule; +import tools.jackson.databind.ObjectMapper; + +public class JRefBeanDeserializerTest { + + static class Human { + @JsonProperty + String name; + @JsonProperty + Human parent; + @JsonProperty + Map props; + @JsonProperty + Human o; + + public Human() { + } + + @Override + public String toString() { + return "Human[name=" + name + ", parent=" + parent + ", props=" + props + ", o=" + this.o + "]"; + } + + } + + static class Message { + @JsonProperty + List items; + + public Message(List items) { + this.items = items; + } + + @Override + public String toString() { + return "Message[items=" + items + "]"; + } + } + + @Test + public void testJRef() throws Exception { + ObjectMapper mapper = jsonMapperBuilder().addModule(new JRefModule()) + .build(); + + String message = "{\r\n" + + " \"items\" : [ {\r\n" + + " \"name\" : \"wendy\",\r\n" + + " \"parent\" : {\r\n" + + " \"name\" : \"sam\",\r\n" + + " \"parent\" : null,\r\n" + + " \"props\" : {\r\n" + + " \"s1\" : 1\r\n" + + " }\r\n" + + " },\r\n" + + " \"props\" : {\r\n" + + " \"q\" : \"r\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"name\" : \"rick\",\r\n" + + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/items/0/parent\"\r\n" + + " },\r\n" + + " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" + + " } ]\r\n" + + "}"; + + Message msg = mapper.readValue(message, Message.class); + System.out.println(msg); + } + +} From 2e59cee97dc22e8ddb263728bde8086810214315 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Wed, 20 May 2026 21:44:11 -0700 Subject: [PATCH 02/10] Added JRefSetter and JRefSetterFunction types, simplified JRefPath and JRefResolver. --- .../java/tools/jackson/databind/JRefPath.java | 37 +++++++++++++++++ .../databind/JRefResolveException.java | 2 +- .../tools/jackson/databind/JRefResolver.java | 41 +++++++------------ .../tools/jackson/databind/JRefSetter.java | 6 +++ .../jackson/databind/JRefSetterFunction.java | 18 ++++++++ .../databind/JRefValueDeserializer.java | 2 +- .../databind/deser/impl/MethodProperty.java | 15 ++++--- 7 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 src/main/java/tools/jackson/databind/JRefPath.java create mode 100644 src/main/java/tools/jackson/databind/JRefSetter.java create mode 100644 src/main/java/tools/jackson/databind/JRefSetterFunction.java diff --git a/src/main/java/tools/jackson/databind/JRefPath.java b/src/main/java/tools/jackson/databind/JRefPath.java new file mode 100644 index 0000000000..185a4bee2b --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefPath.java @@ -0,0 +1,37 @@ +package tools.jackson.databind; + +import java.util.Objects; + +public class JRefPath { + + private final DeserializationContext ctxt; + private final String path; + private final ValueDeserializer deserializer; + + public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer) { + Objects.requireNonNull(path, "path cannot be null"); + this.path = path; + Objects.requireNonNull(ctxt, "ctxt cannot be null"); + this.ctxt = ctxt; + Objects.requireNonNull(deserializer, "deserializer cannot be null"); + this.deserializer = deserializer; + } + + public DeserializationContext getContext() { + return this.ctxt; + } + + public String getPath() { + return this.path; + } + + public ValueDeserializer getValueDeserializer() { + return deserializer; + } + + @Override + public String toString() { + return "JRefPath [ctxt=" + ctxt + ", path=" + path + ", deserializer=" + deserializer + "]"; + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefResolveException.java b/src/main/java/tools/jackson/databind/JRefResolveException.java index 0380273b12..c92f932a6f 100644 --- a/src/main/java/tools/jackson/databind/JRefResolveException.java +++ b/src/main/java/tools/jackson/databind/JRefResolveException.java @@ -6,7 +6,7 @@ public class JRefResolveException extends RuntimeException { private final JRefResolver resolver; private final Object root; - public JRefResolveException(JRefResolver resolver, Object root, String message, Exception cause) { + public JRefResolveException(JRefResolver resolver, Object root, String message, Throwable cause) { super(message, cause); this.resolver = resolver; this.root = root; diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java index ea5a0ade11..652864275c 100644 --- a/src/main/java/tools/jackson/databind/JRefResolver.java +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -9,41 +9,28 @@ import java.util.Objects; import java.util.function.Function; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.deser.impl.MethodProperty; - public class JRefResolver { - private final DeserializationContext ctxt; - private final String path; - private MethodProperty methodProperty; - private Object targetInstance; + private final JRefPath jrefPath; + private final JRefSetter settable; - public JRefResolver(DeserializationContext ctxt, String path) { - Objects.requireNonNull(ctxt, "deserialization context must not be null"); - this.ctxt = ctxt; - Objects.requireNonNull(path, "path must must not be null"); - this.path = path; + public JRefResolver(JRefPath jrefPath, JRefSetter settable) { + Objects.requireNonNull(jrefPath, "jrefPath must not be null"); + this.jrefPath = jrefPath; + Objects.requireNonNull(settable, "settable must must not be null"); + this.settable = settable; } - public void setSetter(MethodProperty methodProperty, Object targetInstance) { - this.methodProperty = methodProperty; - this.targetInstance = targetInstance; - } - - public void resolve(Object root) throws JRefResolveException { + public Object resolve(Object root) throws JRefResolveException { if (root == null) { throw new JRefResolveException(this, "Root object cannot be null"); } - if (this.methodProperty == null) { - throw new JRefResolveException(this, root, "methodProperty is null. methodProperty must be set prior to calling resolve"); - } // Now that we have the root, we can lookup the object at path Object value = resolvePathToValue(root); try { - this.methodProperty.set(ctxt, targetInstance, value); - } catch (JacksonException e) { - throw new JRefResolveException(this, root, "Exception setting value", e); + return this.settable.setInstanceToValue(value); + } catch (Throwable e) { + throw new JRefResolveException(this, root, "Exception setting value=" + value, e); } } @@ -167,16 +154,16 @@ protected String append(Object segment, String pointer) { } protected Object resolvePathToValue(Object root) { - String refStr = this.path; + String refStr = this.jrefPath.getPath(); String[] parts = refStr.split("#", 2); if (parts.length > 1) { Object refValue = get(decode_uri(parts[1]), root); if (refValue == null) { - throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.path + " not found on root=" + root); + throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.jrefPath.getPath() + " not found on root=" + root); } return refValue; } - throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.path + " does not have preceding '#'"); + throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.jrefPath.getPath() + " does not have preceding '#'"); } } diff --git a/src/main/java/tools/jackson/databind/JRefSetter.java b/src/main/java/tools/jackson/databind/JRefSetter.java new file mode 100644 index 0000000000..86111c3b9a --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefSetter.java @@ -0,0 +1,6 @@ +package tools.jackson.databind; + +public abstract class JRefSetter { + + protected abstract Object setInstanceToValue(Object value) throws Throwable; +} diff --git a/src/main/java/tools/jackson/databind/JRefSetterFunction.java b/src/main/java/tools/jackson/databind/JRefSetterFunction.java new file mode 100644 index 0000000000..354008ac29 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefSetterFunction.java @@ -0,0 +1,18 @@ +package tools.jackson.databind; + +import java.util.function.Function; + +public class JRefSetterFunction extends JRefSetter { + + private final Function settingFunction; + + public JRefSetterFunction(Function func) { + this.settingFunction = func; + } + + @Override + protected Object setInstanceToValue(Object value) throws Throwable { + return this.settingFunction.apply(value); + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java index 36352ddd91..db0b57e182 100644 --- a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java @@ -38,7 +38,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws Jack if (valNode != null) { String valStr = valNode.asString(); if (valStr != null) { - return new JRefResolver(ctxt, valStr); + return new JRefPath(valStr, ctxt, this); } } } diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index c2fe09dd53..ef8008d2f6 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -131,17 +131,22 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } // XXX JREF handling - if (value instanceof JRefResolver) { - JRefResolver r = (JRefResolver) value; - // pass in setter - r.setSetter(this, instance); + if (value instanceof JRefPath) { + JRefResolver resolver = new JRefResolver((JRefPath) value, new JRefSetterFunction((v) -> { + try { + _setter.get().invokeExact(instance, v); + } catch (Throwable e) { + _throwAsJacksonE(p, e, v); + } + return instance; + })); @SuppressWarnings("unchecked") List jrefs = (List) ctxt.getAttribute("jrefs"); if (jrefs == null) { jrefs = new ArrayList(); ctxt.setAttribute("jrefs", jrefs); } - jrefs.add(r); + jrefs.add(resolver); } else { try { _setter.get().invokeExact(instance, value); From d7e2e6b98adbf4b178ad2a4c9a5942c9b3a8870b Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Thu, 21 May 2026 14:26:51 -0700 Subject: [PATCH 03/10] Removed JRefSetter and JRefSetterFunction in favor of new SetterFunction functional interface. Added test case for testing second array entry jref to first entry. --- .../tools/jackson/databind/JRefResolver.java | 10 ++--- .../tools/jackson/databind/JRefSetter.java | 6 --- .../jackson/databind/JRefSetterFunction.java | 18 -------- .../databind/JRefValueDeserializer.java | 2 +- .../jackson/databind/SetterFunction.java | 7 +++ .../jackson/databind/ValueDeserializer.java | 24 ++++++++++ .../databind/deser/impl/MethodProperty.java | 37 +++++++--------- .../deser/jdk/CollectionDeserializer.java | 7 ++- .../databind/deser/jdk/MapDeserializer.java | 44 +++++++++++++++---- .../deser/bean/JRefBeanDeserializerTest.java | 21 +++++++-- 10 files changed, 110 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/tools/jackson/databind/JRefSetter.java delete mode 100644 src/main/java/tools/jackson/databind/JRefSetterFunction.java create mode 100644 src/main/java/tools/jackson/databind/SetterFunction.java diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java index 652864275c..252d1654fe 100644 --- a/src/main/java/tools/jackson/databind/JRefResolver.java +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -12,13 +12,13 @@ public class JRefResolver { private final JRefPath jrefPath; - private final JRefSetter settable; + private final SetterFunction setter; - public JRefResolver(JRefPath jrefPath, JRefSetter settable) { + public JRefResolver(JRefPath jrefPath, SetterFunction setter) { Objects.requireNonNull(jrefPath, "jrefPath must not be null"); this.jrefPath = jrefPath; - Objects.requireNonNull(settable, "settable must must not be null"); - this.settable = settable; + Objects.requireNonNull(setter, "setter function must must not be null"); + this.setter = setter; } public Object resolve(Object root) throws JRefResolveException { @@ -28,7 +28,7 @@ public Object resolve(Object root) throws JRefResolveException { // Now that we have the root, we can lookup the object at path Object value = resolvePathToValue(root); try { - return this.settable.setInstanceToValue(value); + return this.setter.set(value); } catch (Throwable e) { throw new JRefResolveException(this, root, "Exception setting value=" + value, e); } diff --git a/src/main/java/tools/jackson/databind/JRefSetter.java b/src/main/java/tools/jackson/databind/JRefSetter.java deleted file mode 100644 index 86111c3b9a..0000000000 --- a/src/main/java/tools/jackson/databind/JRefSetter.java +++ /dev/null @@ -1,6 +0,0 @@ -package tools.jackson.databind; - -public abstract class JRefSetter { - - protected abstract Object setInstanceToValue(Object value) throws Throwable; -} diff --git a/src/main/java/tools/jackson/databind/JRefSetterFunction.java b/src/main/java/tools/jackson/databind/JRefSetterFunction.java deleted file mode 100644 index 354008ac29..0000000000 --- a/src/main/java/tools/jackson/databind/JRefSetterFunction.java +++ /dev/null @@ -1,18 +0,0 @@ -package tools.jackson.databind; - -import java.util.function.Function; - -public class JRefSetterFunction extends JRefSetter { - - private final Function settingFunction; - - public JRefSetterFunction(Function func) { - this.settingFunction = func; - } - - @Override - protected Object setInstanceToValue(Object value) throws Throwable { - return this.settingFunction.apply(value); - } - -} diff --git a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java index db0b57e182..231824352a 100644 --- a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java @@ -38,7 +38,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws Jack if (valNode != null) { String valStr = valNode.asString(); if (valStr != null) { - return new JRefPath(valStr, ctxt, this); + return new JRefPath(valStr, ctxt, getDelegatee()); } } } diff --git a/src/main/java/tools/jackson/databind/SetterFunction.java b/src/main/java/tools/jackson/databind/SetterFunction.java new file mode 100644 index 0000000000..926e353290 --- /dev/null +++ b/src/main/java/tools/jackson/databind/SetterFunction.java @@ -0,0 +1,7 @@ +package tools.jackson.databind; + +@FunctionalInterface +public interface SetterFunction { + + public Object set(Object v) throws Throwable; +} diff --git a/src/main/java/tools/jackson/databind/ValueDeserializer.java b/src/main/java/tools/jackson/databind/ValueDeserializer.java index 34d1ff8509..786554ce22 100644 --- a/src/main/java/tools/jackson/databind/ValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/ValueDeserializer.java @@ -1,6 +1,8 @@ package tools.jackson.databind; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Set; import tools.jackson.core.JacksonException; @@ -498,6 +500,28 @@ public boolean hasAnySetter() { return false; } + // XXX JREF New method for supporting JREF for deserialization + public void setWithJRef(DeserializationContext ctxt, Object value, SetterFunction cb) throws RuntimeException { + if (value instanceof JRefPath) { + JRefResolver resolver = new JRefResolver((JRefPath) value, cb); + @SuppressWarnings("unchecked") + List jrefs = (List) ctxt.getAttribute("jrefs"); + if (jrefs == null) { + jrefs = new ArrayList(); + ctxt.setAttribute("jrefs", jrefs); + } + jrefs.add(resolver); + } else { + try { + cb.set(value); + } catch (Throwable e) { + if (e instanceof RuntimeException) throw (RuntimeException) e; + else throw new RuntimeException(e); + } + } + } + + /* /********************************************************************** /* Helper classes diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index ef8008d2f6..e7faabe109 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -13,6 +13,7 @@ import tools.jackson.databind.*; import tools.jackson.databind.deser.NullValueProvider; import tools.jackson.databind.deser.SettableBeanProperty; +import tools.jackson.databind.deser.std.StdDeserializer; import tools.jackson.databind.introspect.*; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.util.Annotations; @@ -130,30 +131,22 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - // XXX JREF handling - if (value instanceof JRefPath) { - JRefResolver resolver = new JRefResolver((JRefPath) value, new JRefSetterFunction((v) -> { - try { - _setter.get().invokeExact(instance, v); - } catch (Throwable e) { - _throwAsJacksonE(p, e, v); - } - return instance; - })); - @SuppressWarnings("unchecked") - List jrefs = (List) ctxt.getAttribute("jrefs"); - if (jrefs == null) { - jrefs = new ArrayList(); - ctxt.setAttribute("jrefs", jrefs); - } - jrefs.add(resolver); - } else { - try { - _setter.get().invokeExact(instance, value); + + // XXX JREF handling. ValueDeserializer.setWithJRef + // checks the return value, and if type JRefPath + // it defers calling the provided SetterFunction until + // the value is resolved (after root deserialization complete). + // if value is not of type JRefPath, then the given setter function + // is called immediately with the given value + _valueDeserializer.setWithJRef(ctxt, value, (v) -> { + try { + _setter.get().invokeExact(instance, v); } catch (Throwable e) { - _throwAsJacksonE(p, e, value); + _throwAsJacksonE(p, e, v); } - } + return instance; + }); + } @Override diff --git a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java index 5db454e973..4966858013 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java @@ -398,8 +398,11 @@ protected Collection _deserializeFromArray(JsonParser p, Deserialization continue; } } - - result.add(value); + // XXX JREF handling + setWithJRef(ctxt, value, (v) -> { + result.add(v); + return result; + }); /* 17-Dec-2017, tatu: should not occur at this level... } catch (UnresolvedForwardReference reference) { diff --git a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java index ced9b092e9..af5c87b27f 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java @@ -1,15 +1,36 @@ package tools.jackson.databind.deser.jdk; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIncludeProperties; -import tools.jackson.core.*; -import tools.jackson.databind.*; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonTokenId; +import tools.jackson.core.StreamReadCapability; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.annotation.JacksonStdImpl; -import tools.jackson.databind.deser.*; +import tools.jackson.databind.deser.ContextualKeyDeserializer; +import tools.jackson.databind.deser.NullValueProvider; import tools.jackson.databind.deser.ReadableObjectId.Referring; +import tools.jackson.databind.deser.SettableBeanProperty; +import tools.jackson.databind.deser.UnresolvedForwardReference; +import tools.jackson.databind.deser.ValueInstantiator; import tools.jackson.databind.deser.bean.PropertyBasedCreator; import tools.jackson.databind.deser.bean.PropertyValueBuffer; import tools.jackson.databind.deser.std.ContainerDeserializerBase; @@ -668,10 +689,15 @@ protected final Map _readAndBindStringKeyMap(JsonParser p, Deseri if (useObjectId) { referringAccumulator.put(key, value); } else { - Object oldValue = result.put(key, value); - if (oldValue != null) { - _squashDups(ctxt, result, key, oldValue, value); - } + // XXX JREF + final String k = key; + this.setWithJRef(ctxt, value, (v) -> { + Object oldValue = result.put(k, v); + if (oldValue != null) { + _squashDups(ctxt, result, k, oldValue, v); + } + return result; + }); } } catch (UnresolvedForwardReference reference) { handleUnresolvedReference(ctxt, referringAccumulator, key, reference); @@ -684,7 +710,7 @@ protected final Map _readAndBindStringKeyMap(JsonParser p, Deseri return result; } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") public Map _deserializeUsingCreator(JsonParser p, DeserializationContext ctxt) throws JacksonException { diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index 396b110317..43dee9ec5f 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import org.junit.Assert; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonProperty; @@ -48,10 +49,23 @@ public String toString() { } } + protected ObjectMapper buildObjectMapperWithJRefSupport() { + return jsonMapperBuilder().addModule(new JRefModule()) + .build(); + } + + @Test + public void testCollectionItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0), msg.items.get(1)); + } @Test public void testJRef() throws Exception { - ObjectMapper mapper = jsonMapperBuilder().addModule(new JRefModule()) - .build(); + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); String message = "{\r\n" + " \"items\" : [ {\r\n" @@ -64,7 +78,8 @@ public void testJRef() throws Exception { + " }\r\n" + " },\r\n" + " \"props\" : {\r\n" - + " \"q\" : \"r\"\r\n" + + " \"q\" : \"r\",\r\n" + + " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" + " }\r\n" + " }, {\r\n" + " \"name\" : \"rick\",\r\n" From 805fc1b985d485b29b26dd8049e75533950d6772 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Fri, 22 May 2026 16:36:00 -0700 Subject: [PATCH 04/10] Added support in ValueDeserializerModifier for using JRefValueDeserializer wrapper for all types of ValueDeserializers (except for Reference types). See JRefModule for definition of JRefValueDeserializerModifier. --- .../tools/jackson/databind/JRefModule.java | 95 ++++++++++++++-- .../java/tools/jackson/databind/JRefPath.java | 26 +++-- .../databind/JRefResolveException.java | 10 +- .../tools/jackson/databind/JRefResolver.java | 13 +-- .../databind/JRefValueDeserializer.java | 102 ++++++++++++++---- .../deser/bean/JRefBeanDeserializerTest.java | 29 ++++- 6 files changed, 229 insertions(+), 46 deletions(-) diff --git a/src/main/java/tools/jackson/databind/JRefModule.java b/src/main/java/tools/jackson/databind/JRefModule.java index 103caa4cb6..c52ab1fc20 100644 --- a/src/main/java/tools/jackson/databind/JRefModule.java +++ b/src/main/java/tools/jackson/databind/JRefModule.java @@ -1,8 +1,18 @@ package tools.jackson.databind; +import java.util.List; + import tools.jackson.databind.BeanDescription.Supplier; +import tools.jackson.databind.deser.BeanDeserializerBuilder; import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.introspect.BeanPropertyDefinition; import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.type.ArrayType; +import tools.jackson.databind.type.CollectionLikeType; +import tools.jackson.databind.type.CollectionType; +import tools.jackson.databind.type.MapLikeType; +import tools.jackson.databind.type.MapType; +import tools.jackson.databind.type.ReferenceType; public class JRefModule extends SimpleModule { @@ -12,16 +22,85 @@ public JRefModule() { super("JRef"); } + protected class JRefValueDeserializerModifier extends ValueDeserializerModifier { + + private static final long serialVersionUID = 1L; + + protected ValueDeserializer makeJRefValueDeserializer(DeserializationConfig config, JavaType jt, + Supplier beanDescRef, ValueDeserializer d) { + return new JRefValueDeserializer(d); + } + + @Override + public ValueDeserializer modifyArrayDeserializer(DeserializationConfig config, ArrayType valueType, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, valueType, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyCollectionLikeDeserializer(DeserializationConfig config, + CollectionLikeType type, Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, Supplier beanDescRef, + ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, null, beanDescRef, deserializer); + } + + @Override + public KeyDeserializer modifyKeyDeserializer(DeserializationConfig config, JavaType type, + KeyDeserializer deserializer) { + return super.modifyKeyDeserializer(config, type, deserializer); + } + + @Override + public ValueDeserializer modifyMapDeserializer(DeserializationConfig config, MapType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, Supplier beanDescRef, + BeanDeserializerBuilder builder) { + return super.updateBuilder(config, beanDescRef, builder); + } + + @Override + public List updateProperties(DeserializationConfig config, Supplier beanDescRef, + List propDefs) { + return super.updateProperties(config, beanDescRef, propDefs); + } + + @Override + public ValueDeserializer modifyReferenceDeserializer(DeserializationConfig config, ReferenceType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return super.modifyReferenceDeserializer(config, type, beanDescRef, deserializer); + } + } + @Override public void setupModule(SetupContext context) { super.setupModule(context); - context.addDeserializerModifier(new ValueDeserializerModifier() { - @Override - public ValueDeserializer modifyDeserializer(DeserializationConfig config, Supplier beanDescRef, - ValueDeserializer deserializer) { - return new JRefValueDeserializer(deserializer); - } - }); + context.addDeserializerModifier(new JRefValueDeserializerModifier()); } } - diff --git a/src/main/java/tools/jackson/databind/JRefPath.java b/src/main/java/tools/jackson/databind/JRefPath.java index 185a4bee2b..276a8d8c41 100644 --- a/src/main/java/tools/jackson/databind/JRefPath.java +++ b/src/main/java/tools/jackson/databind/JRefPath.java @@ -2,25 +2,34 @@ import java.util.Objects; +import tools.jackson.databind.jsontype.TypeDeserializer; + public class JRefPath { private final DeserializationContext ctxt; private final String path; private final ValueDeserializer deserializer; - - public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer) { + private final TypeDeserializer typeDeserializer; + + public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer, + TypeDeserializer typeDeserializer) { Objects.requireNonNull(path, "path cannot be null"); this.path = path; Objects.requireNonNull(ctxt, "ctxt cannot be null"); this.ctxt = ctxt; Objects.requireNonNull(deserializer, "deserializer cannot be null"); this.deserializer = deserializer; + this.typeDeserializer = typeDeserializer; + } + + public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer) { + this(path, ctxt, deserializer, null); } - + public DeserializationContext getContext() { return this.ctxt; } - + public String getPath() { return this.path; } @@ -29,9 +38,14 @@ public ValueDeserializer getValueDeserializer() { return deserializer; } + public TypeDeserializer getTypeDeserializer() { + return typeDeserializer; + } + @Override public String toString() { - return "JRefPath [ctxt=" + ctxt + ", path=" + path + ", deserializer=" + deserializer + "]"; + return "JRefPath[ctxt=" + ctxt + ", path=" + path + ", deserializer=" + deserializer + ", typeDeserializer=" + + typeDeserializer + "]"; } - + } diff --git a/src/main/java/tools/jackson/databind/JRefResolveException.java b/src/main/java/tools/jackson/databind/JRefResolveException.java index c92f932a6f..2f9cdd7524 100644 --- a/src/main/java/tools/jackson/databind/JRefResolveException.java +++ b/src/main/java/tools/jackson/databind/JRefResolveException.java @@ -5,27 +5,27 @@ public class JRefResolveException extends RuntimeException { private static final long serialVersionUID = 1L; private final JRefResolver resolver; private final Object root; - + public JRefResolveException(JRefResolver resolver, Object root, String message, Throwable cause) { super(message, cause); this.resolver = resolver; this.root = root; } - + public JRefResolveException(JRefResolver resolver, Object root, String message) { super(message); this.resolver = resolver; this.root = root; } - + public JRefResolveException(JRefResolver resolver, String message) { this(resolver, null, message); } - + public JRefResolver getResolver() { return this.resolver; } - + public Object getRoot() { return this.root; } diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java index 252d1654fe..95dac42e9f 100644 --- a/src/main/java/tools/jackson/databind/JRefResolver.java +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -13,14 +13,14 @@ public class JRefResolver { private final JRefPath jrefPath; private final SetterFunction setter; - + public JRefResolver(JRefPath jrefPath, SetterFunction setter) { Objects.requireNonNull(jrefPath, "jrefPath must not be null"); this.jrefPath = jrefPath; Objects.requireNonNull(setter, "setter function must must not be null"); this.setter = setter; } - + public Object resolve(Object root) throws JRefResolveException { if (root == null) { throw new JRefResolveException(this, "Root object cannot be null"); @@ -41,7 +41,7 @@ private static String decode_uri(String uri) { return uri; } } - + protected String unescape(String segment) { return segment.replace("~1", "/").replace("~0", "~"); } @@ -139,7 +139,6 @@ protected Object getAccessibleFieldValue(Object value, String fieldName) { } } - protected Object _get(Iterable segments, Object subject) { String cursor = ""; for (String segment : segments) { @@ -159,11 +158,13 @@ protected Object resolvePathToValue(Object root) { if (parts.length > 1) { Object refValue = get(decode_uri(parts[1]), root); if (refValue == null) { - throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.jrefPath.getPath() + " not found on root=" + root); + throw new JRefResolveException(this, root, + "Invalid local reference: path=" + this.jrefPath.getPath() + " not found on root=" + root); } return refValue; } - throw new JRefResolveException(this, root, "Invalid local reference: path=" + this.jrefPath.getPath() + " does not have preceding '#'"); + throw new JRefResolveException(this, root, + "Invalid local reference: path=" + this.jrefPath.getPath() + " does not have preceding '#'"); } } diff --git a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java index 231824352a..612b876bbe 100644 --- a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java @@ -1,20 +1,26 @@ package tools.jackson.databind; +import java.util.function.Function; + import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.node.ObjectNode; import tools.jackson.databind.node.TreeTraversingParser; public class JRefValueDeserializer extends DelegatingDeserializer { public static final String JREF = "$ref"; - + void trace(String method, JRefValueDeserializer vds) { - StringBuffer buf = new StringBuffer(method).append("."); - buf.append(vds.toString()); - System.out.println(buf.toString()); + System.out.println(new StringBuffer(method).append(".").append(vds.toString().toString())); + } + + @Override + public String toString() { + return "JRefVD[d=" + _delegatee + ", class=" + _valueClass + "]"; } void trace(String method) { @@ -25,31 +31,89 @@ public JRefValueDeserializer(ValueDeserializer src) { super(src); } - @Override - public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - trace("deserialize"); - Object result = null; - JsonToken current = p.currentToken(); - if (current == JsonToken.START_OBJECT) { - JsonNode node = ctxt.readTree(p); - if (node instanceof ObjectNode) { - ObjectNode onode = (ObjectNode) node; - JsonNode valNode = onode.get(JREF); + protected class JRefFindResult { + JRefPath jrefPath; + JsonParser parser; + } + + /** + * Check for JRef in parser with context. + * + * @param p JsonParser to use + * @param ctxt the current context + * @param typeDeserializer optional typeDeserializer + * @return JRefFindResult with JRefPath either set to non null (path found), or + * set to null (meaning that the further deserialization should be + * undertaken with the parse given in JRefFindResult + */ + protected JRefFindResult checkForJRef(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) { + JRefFindResult result = new JRefFindResult(); + JsonToken tok = p.currentToken(); + if (tok == JsonToken.START_OBJECT) { + JsonNode n = ctxt.readTree(p); + if (n instanceof ObjectNode) { + ObjectNode on = (ObjectNode) n; + JsonNode valNode = on.get(JREF); if (valNode != null) { String valStr = valNode.asString(); if (valStr != null) { - return new JRefPath(valStr, ctxt, getDelegatee()); + result.jrefPath = new JRefPath(valStr, ctxt, this, typeDeserializer); + result.parser = p; + return result; } } } - TreeTraversingParser tpp = new TreeTraversingParser(node); + @SuppressWarnings("resource") + TreeTraversingParser tpp = new TreeTraversingParser(n); tpp.nextToken(); - result = super.deserialize(tpp, ctxt); + result.parser = tpp; + } else { + result.parser = p; + } + return result; + } + + /** + * Deserialize with jref handling. Calls + * {@link #checkForJRef(JsonParser, DeserializationContext, TypeDeserializer)} + * to look on the parser stream for objects with 1 key == '$ref'. If found then + * the JRefPath is returned. If not found then the func arg is applied with the + * appropriate parser returned in JRefFindResult.parser field. + * + * @param p + * @param ctxt + * @param typeDeserializer + * @param func + * @return + */ + protected Object deserializerWithJRef(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer, + Function func) { + String method = "deserialize" + ((typeDeserializer == null) ? "" : "WithType"); + trace(method); + Object result = null; + JRefFindResult findResult = checkForJRef(p, ctxt, typeDeserializer); + if (findResult.jrefPath != null) { + trace(" JRefPath=" + findResult.jrefPath); + result = findResult.jrefPath; } else { - result = super.deserialize(p, ctxt); + result = func.apply(findResult.parser); } - trace("deserialized result=" + result); + trace(method + " result=" + result); return result; + + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + throws JacksonException { + return deserializerWithJRef(p, ctxt, typeDeserializer, + ps -> super.deserializeWithType(ps, ctxt, typeDeserializer)); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + return deserializerWithJRef(p, ctxt, null, ps -> super.deserialize(ps, ctxt)); } @Override diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index 43dee9ec5f..47e11c8a66 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -24,7 +24,11 @@ static class Human { Map props; @JsonProperty Human o; - + @JsonProperty + String otherName; + @JsonProperty + Map moreProps; + public Human() { } @@ -55,7 +59,17 @@ protected ObjectMapper buildObjectMapperWithJRefSupport() { } @Test - public void testCollectionItemPath() throws Exception { + public void testStringItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 }, \"otherName\": { \"$ref\": \"#/items/0/name\" } }]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); + } + + @Test + public void testCollectionStringKeyItemPath() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); // Input has first item in Message.items list fully defined, and second item jrefs to first item String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; @@ -63,6 +77,17 @@ public void testCollectionItemPath() throws Exception { Message msg = mapper.readValue(message, Message.class); Assert.assertEquals(msg.items.get(0), msg.items.get(1)); } + + @Test + public void testCollectionObjectKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"name\": \"wendy\", \"parent\": null, \"moreProps\": { \"$ref\": \"#/items/0/props\" }}]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); + } + @Test public void testJRef() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); From 36ef70c3448a782918ef9578594acaa98fc103ab Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Sun, 24 May 2026 13:36:16 -0700 Subject: [PATCH 05/10] Added jref handling to StringCollectionDeserializer. Added test case tools.jackson.databind.deser.bean.JRefBeanDeserializerTest.testMyStringsJRefPath() to test --- .../deser/jdk/StringCollectionDeserializer.java | 10 +++++++--- .../deser/bean/JRefBeanDeserializerTest.java | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java index 405dab1b60..6fc0a01c7c 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java @@ -286,7 +286,8 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC * notably XML. Note, however, that while we can get String, we can't * assume that's what we use due to custom deserializer */ - String value; + // XXX JRef + Object value; if (p.nextStringValue() == null) { JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { @@ -312,8 +313,11 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC continue; } } - - result.add(value); + // XXX JREF + setWithJRef(ctxt, value, (v) -> { + result.add((String) v); + return result; + }); } } catch (Exception e) { throw DatabindException.wrapWithPath(ctxt, e, diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index 47e11c8a66..7490c19524 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -1,5 +1,6 @@ package tools.jackson.databind.deser.bean; +import static org.junit.jupiter.api.Assertions.assertEquals; import static tools.jackson.databind.testutil.DatabindTestUtil.jsonMapperBuilder; import java.util.List; @@ -15,6 +16,18 @@ public class JRefBeanDeserializerTest { + static class M { + @JsonProperty + List items; + } + @Test + public void testMyStringsJRefPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }]}"; + M result = mapper.readValue(input, M.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + static class Human { @JsonProperty String name; From fbc2d63c56505fe8a0972677cabc0d2df9893d9c Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Sun, 24 May 2026 14:21:00 -0700 Subject: [PATCH 06/10] Added test cases for Double, Float, Boolean, and String handling in collections with jrefs. --- .../deser/bean/JRefBeanDeserializerTest.java | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index 7490c19524..195edbc1e9 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -16,18 +16,73 @@ public class JRefBeanDeserializerTest { - static class M { + static class StringItems { @JsonProperty List items; + @JsonProperty + String second; } + @Test - public void testMyStringsJRefPath() throws Exception { + public void testStringItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); - String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }]}"; - M result = mapper.readValue(input, M.class); + String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }], \"second\": { \"$ref\": \"#/items/0\" }}"; + StringItems result = mapper.readValue(input, StringItems.class); assertEquals(result.items.get(0), result.items.get(1)); + assertEquals(result.items.get(0), result.second); + } + static class IntegerItems { + @JsonProperty + List items; } + @Test + public void testIntegerItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5, { \"$ref\": \"#/items/0\" }]}"; + IntegerItems result = mapper.readValue(input, IntegerItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class DoubleItems { + @JsonProperty + List items; + } + + @Test + public void testDoubleItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + DoubleItems result = mapper.readValue(input, DoubleItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class FloatItems { + @JsonProperty + List items; + } + + @Test + public void testFloatItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + FloatItems result = mapper.readValue(input, FloatItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class BooleanItems { + @JsonProperty + List items; + } + + @Test + public void testBooleanItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[true, { \"$ref\": \"#/items/0\" }]}"; + BooleanItems result = mapper.readValue(input, BooleanItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + static class Human { @JsonProperty String name; From d038844826484ab99031fd8e75ab7f4731f934e0 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Sun, 24 May 2026 19:03:15 -0700 Subject: [PATCH 07/10] Added more (passing) test cases --- .../deser/bean/JRefBeanDeserializerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index 195edbc1e9..b7511fb18d 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -16,6 +16,27 @@ public class JRefBeanDeserializerTest { + static class IntType { + @JsonProperty + int i; + } + + static class IntItems { + @JsonProperty + int j; + @JsonProperty + List items; + } + + @Test + public void testIntItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"j\": 20, \"items\":[ { \"i\": 10}, { \"i\": { \"$ref\": \"#/items/0/i\" }}, { \"i\": { \"$ref\": \"#/j\" }}]}"; + IntItems result = mapper.readValue(input, IntItems.class); + assertEquals(result.items.get(0).i, result.items.get(1).i); + assertEquals(result.j, result.items.get(2).i); + } + static class StringItems { @JsonProperty List items; From 5fd7a244df3b3c93e84e1fa85208e089b0eff140 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Mon, 25 May 2026 16:43:55 -0700 Subject: [PATCH 08/10] Refactored for simplicity. Added string constants for "$ref" and "jrefs" deserializationcontext attribute name. --- .../tools/jackson/databind/JRefModule.java | 120 +++++++++++++++++ .../java/tools/jackson/databind/JRefPath.java | 1 + .../databind/JRefResolveException.java | 7 + .../tools/jackson/databind/JRefResolver.java | 2 + .../databind/JRefValueDeserializer.java | 124 ------------------ .../tools/jackson/databind/ObjectMapper.java | 23 +++- .../jackson/databind/ValueDeserializer.java | 34 +++-- .../deser/bean/JRefBeanDeserializerTest.java | 89 ++++++------- 8 files changed, 208 insertions(+), 192 deletions(-) delete mode 100644 src/main/java/tools/jackson/databind/JRefValueDeserializer.java diff --git a/src/main/java/tools/jackson/databind/JRefModule.java b/src/main/java/tools/jackson/databind/JRefModule.java index c52ab1fc20..e4824e38fc 100644 --- a/src/main/java/tools/jackson/databind/JRefModule.java +++ b/src/main/java/tools/jackson/databind/JRefModule.java @@ -1,12 +1,20 @@ package tools.jackson.databind; import java.util.List; +import java.util.function.Function; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; import tools.jackson.databind.BeanDescription.Supplier; import tools.jackson.databind.deser.BeanDeserializerBuilder; import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.deser.std.DelegatingDeserializer; import tools.jackson.databind.introspect.BeanPropertyDefinition; +import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.TreeTraversingParser; import tools.jackson.databind.type.ArrayType; import tools.jackson.databind.type.CollectionLikeType; import tools.jackson.databind.type.CollectionType; @@ -103,4 +111,116 @@ public void setupModule(SetupContext context) { super.setupModule(context); context.addDeserializerModifier(new JRefValueDeserializerModifier()); } + + public static class JRefValueDeserializer extends DelegatingDeserializer { + + void trace(String method, JRefValueDeserializer vds) { + System.out.println(new StringBuffer(method).append(".").append(vds.toString().toString())); + } + + @Override + public String toString() { + return "JRefValueDeserializer[delegate=" + _delegatee + ", class=" + _valueClass + "]"; + } + + void trace(String method) { + trace(method, this); + } + + public JRefValueDeserializer(ValueDeserializer src) { + super(src); + } + + protected class JRefFindResult { + JRefPath jrefPath; + JsonParser parser; + } + + protected TreeTraversingParser createTreeTraversingParser(JsonNode node) { + TreeTraversingParser result = new TreeTraversingParser(node); + result.nextToken(); + return result; + } + + /** + * Find JRef in parser input stream. + * + * @param p JsonParser to use + * @param ctxt the current context + * @param typeDeserializer optional typeDeserializer + * @return JRefFindResult with JRefPath either set to non null (path found), or + * set to null (meaning that the further deserialization should be + * undertaken with the parse given in JRefFindResult + */ + protected JRefFindResult findJRef(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) { + JRefFindResult result = new JRefFindResult(); + JsonToken tok = p.currentToken(); + if (tok == JsonToken.START_OBJECT) { + JsonNode n = ctxt.readTree(p); + if (n instanceof ObjectNode) { + ObjectNode on = (ObjectNode) n; + JsonNode valNode = on.get(JRefPath.JREF_REF); + if (valNode != null) { + String valStr = valNode.asString(); + if (valStr != null) { + result.jrefPath = new JRefPath(valStr, ctxt, getDelegatee(), typeDeserializer); + result.parser = p; + return result; + } + // JREF_REF found, but no/null path. This is a syntax error + throw DatabindException.from(p, "JRefPath detected on stream but path is null", null); + } + } + result.parser = createTreeTraversingParser(n); + } else { + result.parser = p; + } + return result; + } + + /** + * Deserialize with jref handling. Calls + * {@link #findJRef(JsonParser, DeserializationContext, TypeDeserializer)} to + * look on the parser stream for objects with 1 key == '$ref'. If found then the + * JRefPath is returned. If not found then the func arg is applied with the + * appropriate parser returned in JRefFindResult.parser field. + * + * @param p + * @param ctxt + * @param typeDeserializer + * @param func + * @return + */ + protected Object deserializerWithJRef(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer, Function func) { + Object result = null; + JRefFindResult findResult = findJRef(p, ctxt, typeDeserializer); + if (findResult.jrefPath != null) { + result = findResult.jrefPath; + } else { + result = func.apply(findResult.parser); + } + return result; + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + throws JacksonException { + return deserializerWithJRef(p, ctxt, typeDeserializer, + ps -> super.deserializeWithType(ps, ctxt, typeDeserializer)); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + return deserializerWithJRef(p, ctxt, null, ps -> super.deserialize(ps, ctxt)); + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new JRefValueDeserializer(newDelegatee); + } + + } + } diff --git a/src/main/java/tools/jackson/databind/JRefPath.java b/src/main/java/tools/jackson/databind/JRefPath.java index 276a8d8c41..04c8a1f231 100644 --- a/src/main/java/tools/jackson/databind/JRefPath.java +++ b/src/main/java/tools/jackson/databind/JRefPath.java @@ -6,6 +6,7 @@ public class JRefPath { + public static final String JREF_REF = "$ref"; private final DeserializationContext ctxt; private final String path; private final ValueDeserializer deserializer; diff --git a/src/main/java/tools/jackson/databind/JRefResolveException.java b/src/main/java/tools/jackson/databind/JRefResolveException.java index 2f9cdd7524..17657291e9 100644 --- a/src/main/java/tools/jackson/databind/JRefResolveException.java +++ b/src/main/java/tools/jackson/databind/JRefResolveException.java @@ -29,4 +29,11 @@ public JRefResolver getResolver() { public Object getRoot() { return this.root; } + + @Override + public String toString() { + return "JRefResolveException [resolver=" + resolver + ", root=" + root + ", message=" + super.getMessage() + + "]"; + } + } diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java index 95dac42e9f..93ed62861b 100644 --- a/src/main/java/tools/jackson/databind/JRefResolver.java +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -11,6 +11,8 @@ public class JRefResolver { + public static final String JREF_RESOLVER_LIST_CONTEXT_ATTR = JRefResolver.class.getName() + ".jrefs"; + private final JRefPath jrefPath; private final SetterFunction setter; diff --git a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java b/src/main/java/tools/jackson/databind/JRefValueDeserializer.java deleted file mode 100644 index 612b876bbe..0000000000 --- a/src/main/java/tools/jackson/databind/JRefValueDeserializer.java +++ /dev/null @@ -1,124 +0,0 @@ -package tools.jackson.databind; - -import java.util.function.Function; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JsonToken; -import tools.jackson.databind.deser.std.DelegatingDeserializer; -import tools.jackson.databind.jsontype.TypeDeserializer; -import tools.jackson.databind.node.ObjectNode; -import tools.jackson.databind.node.TreeTraversingParser; - -public class JRefValueDeserializer extends DelegatingDeserializer { - - public static final String JREF = "$ref"; - - void trace(String method, JRefValueDeserializer vds) { - System.out.println(new StringBuffer(method).append(".").append(vds.toString().toString())); - } - - @Override - public String toString() { - return "JRefVD[d=" + _delegatee + ", class=" + _valueClass + "]"; - } - - void trace(String method) { - trace(method, this); - } - - public JRefValueDeserializer(ValueDeserializer src) { - super(src); - } - - protected class JRefFindResult { - JRefPath jrefPath; - JsonParser parser; - } - - /** - * Check for JRef in parser with context. - * - * @param p JsonParser to use - * @param ctxt the current context - * @param typeDeserializer optional typeDeserializer - * @return JRefFindResult with JRefPath either set to non null (path found), or - * set to null (meaning that the further deserialization should be - * undertaken with the parse given in JRefFindResult - */ - protected JRefFindResult checkForJRef(JsonParser p, DeserializationContext ctxt, - TypeDeserializer typeDeserializer) { - JRefFindResult result = new JRefFindResult(); - JsonToken tok = p.currentToken(); - if (tok == JsonToken.START_OBJECT) { - JsonNode n = ctxt.readTree(p); - if (n instanceof ObjectNode) { - ObjectNode on = (ObjectNode) n; - JsonNode valNode = on.get(JREF); - if (valNode != null) { - String valStr = valNode.asString(); - if (valStr != null) { - result.jrefPath = new JRefPath(valStr, ctxt, this, typeDeserializer); - result.parser = p; - return result; - } - } - } - @SuppressWarnings("resource") - TreeTraversingParser tpp = new TreeTraversingParser(n); - tpp.nextToken(); - result.parser = tpp; - } else { - result.parser = p; - } - return result; - } - - /** - * Deserialize with jref handling. Calls - * {@link #checkForJRef(JsonParser, DeserializationContext, TypeDeserializer)} - * to look on the parser stream for objects with 1 key == '$ref'. If found then - * the JRefPath is returned. If not found then the func arg is applied with the - * appropriate parser returned in JRefFindResult.parser field. - * - * @param p - * @param ctxt - * @param typeDeserializer - * @param func - * @return - */ - protected Object deserializerWithJRef(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer, - Function func) { - String method = "deserialize" + ((typeDeserializer == null) ? "" : "WithType"); - trace(method); - Object result = null; - JRefFindResult findResult = checkForJRef(p, ctxt, typeDeserializer); - if (findResult.jrefPath != null) { - trace(" JRefPath=" + findResult.jrefPath); - result = findResult.jrefPath; - } else { - result = func.apply(findResult.parser); - } - trace(method + " result=" + result); - return result; - - } - - @Override - public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) - throws JacksonException { - return deserializerWithJRef(p, ctxt, typeDeserializer, - ps -> super.deserializeWithType(ps, ctxt, typeDeserializer)); - } - - @Override - public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - return deserializerWithJRef(p, ctxt, null, ps -> super.deserialize(ps, ctxt)); - } - - @Override - protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { - return new JRefValueDeserializer(newDelegatee); - } - -} diff --git a/src/main/java/tools/jackson/databind/ObjectMapper.java b/src/main/java/tools/jackson/databind/ObjectMapper.java index f8f502ce32..30d519c7a1 100644 --- a/src/main/java/tools/jackson/databind/ObjectMapper.java +++ b/src/main/java/tools/jackson/databind/ObjectMapper.java @@ -2667,13 +2667,7 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, _findRootDeserializer(ctxt, valueType), null); ctxt.checkUnresolvedObjectId(); // XXX JREF resolve jrefs - @SuppressWarnings("unchecked") - List jrefs = (List) ctxt.getAttribute("jrefs"); - if (jrefs != null) { - jrefs.forEach(r -> { - r.resolve(result); - }); - } + resolveJRefs(ctxt, result); } if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, valueType); @@ -2682,6 +2676,21 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, } } + /** + * Resolve JRefs found during parsing given the root object + * @param ctxt the deserialization context. Must not be null. + * @param root the root object to resolve with. Should not be null. + */ + protected void resolveJRefs(DeserializationContext ctxt, Object root) { + @SuppressWarnings("unchecked") + List jrefs = (List) ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); + if (jrefs != null) { + jrefs.forEach(r -> { + r.resolve(root); + }); + } + } + /** * Similar to {@link #_readMapAndClose} but specialized for JsonNode reading. */ diff --git a/src/main/java/tools/jackson/databind/ValueDeserializer.java b/src/main/java/tools/jackson/databind/ValueDeserializer.java index a043289b70..fed5f25844 100644 --- a/src/main/java/tools/jackson/databind/ValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/ValueDeserializer.java @@ -481,11 +481,7 @@ public Boolean supportsUpdate(DeserializationConfig config) { } /** - * Method to collect all property names including nested unwrapped properties. - *

- * NOTE: if no names are returned, properties are considered to be - * unknown and caller will NOT assume names are statically known: this - * can affect processing of things like unwrapped properties. + * Method to collect all property names including nested unwrapped properties * * @param names (not null) Set to add property names to; for both regular * and "any" properties. @@ -505,19 +501,35 @@ public boolean hasAnySetter() { } // XXX JREF New method for supporting JREF for deserialization - public void setWithJRef(DeserializationContext ctxt, Object value, SetterFunction cb) throws RuntimeException { + /** + * Set the value with the + * @param ctxt the deserialization context. Must not be null. + * @param value the value to use to set. If an instanceof JRefPath, + * then a JRefResolver is created, and calling the SetterFunction is deferred until after the root + * has been returned. If not JRefPath, then the SetterFunction set is called + * immediately with the value Object. + * @param f the SetterFunction to use to set to the given value. + * @throws RuntimeException if JRefResolver cannot be created, or the + * SetterFunction.set throws a RuntimeException + */ + public void setWithJRef(DeserializationContext ctxt, Object value, SetterFunction f) throws RuntimeException { + // If the value (result of deserialization) is a JRefPath if (value instanceof JRefPath) { - JRefResolver resolver = new JRefResolver((JRefPath) value, cb); @SuppressWarnings("unchecked") - List jrefs = (List) ctxt.getAttribute("jrefs"); + List jrefs = (List) ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); if (jrefs == null) { + // Lazy creation of JRefResolver list jrefs = new ArrayList(); - ctxt.setAttribute("jrefs", jrefs); + ctxt.setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, jrefs); } - jrefs.add(resolver); + // Add this new resolver to the jrefs attribute (List). + // The JRefResolver resolve method is later called to actually + // resolve, given the root object in the object graph + jrefs.add(new JRefResolver((JRefPath) value, f)); } else { + // We simply call the given SetterFunction to set with the value param try { - cb.set(value); + f.set(value); } catch (Throwable e) { if (e instanceof RuntimeException) throw (RuntimeException) e; else throw new RuntimeException(e); diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java index b7511fb18d..467d0d274d 100644 --- a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -20,14 +20,14 @@ static class IntType { @JsonProperty int i; } - + static class IntItems { @JsonProperty int j; @JsonProperty List items; } - + @Test public void testIntItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -43,7 +43,7 @@ static class StringItems { @JsonProperty String second; } - + @Test public void testStringItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -52,11 +52,12 @@ public void testStringItems() throws Exception { assertEquals(result.items.get(0), result.items.get(1)); assertEquals(result.items.get(0), result.second); } + static class IntegerItems { @JsonProperty List items; } - + @Test public void testIntegerItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -69,7 +70,7 @@ static class DoubleItems { @JsonProperty List items; } - + @Test public void testDoubleItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -82,7 +83,7 @@ static class FloatItems { @JsonProperty List items; } - + @Test public void testFloatItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -95,7 +96,7 @@ static class BooleanItems { @JsonProperty List items; } - + @Test public void testBooleanItems() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); @@ -116,8 +117,8 @@ static class Human { @JsonProperty String otherName; @JsonProperty - Map moreProps; - + Map moreProps; + public Human() { } @@ -143,69 +144,57 @@ public String toString() { } protected ObjectMapper buildObjectMapperWithJRefSupport() { - return jsonMapperBuilder().addModule(new JRefModule()) - .build(); + return jsonMapperBuilder().addModule(new JRefModule()).build(); } - + @Test public void testStringItemPath() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); - // Input has first item in Message.items list fully defined, and second item jrefs to first item + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 }, \"otherName\": { \"$ref\": \"#/items/0/name\" } }]}"; - - Message msg = mapper.readValue(message, Message.class); - Assert.assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); } @Test public void testCollectionStringKeyItemPath() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); - // Input has first item in Message.items list fully defined, and second item jrefs to first item + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; - - Message msg = mapper.readValue(message, Message.class); - Assert.assertEquals(msg.items.get(0), msg.items.get(1)); + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0), msg.items.get(1)); } - + @Test public void testCollectionObjectKeyItemPath() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); - // Input has first item in Message.items list fully defined, and second item jrefs to first item + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"name\": \"wendy\", \"parent\": null, \"moreProps\": { \"$ref\": \"#/items/0/props\" }}]}"; - - Message msg = mapper.readValue(message, Message.class); - Assert.assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); } @Test public void testJRef() throws Exception { ObjectMapper mapper = buildObjectMapperWithJRefSupport(); - String message = "{\r\n" - + " \"items\" : [ {\r\n" - + " \"name\" : \"wendy\",\r\n" - + " \"parent\" : {\r\n" - + " \"name\" : \"sam\",\r\n" - + " \"parent\" : null,\r\n" - + " \"props\" : {\r\n" - + " \"s1\" : 1\r\n" - + " }\r\n" - + " },\r\n" - + " \"props\" : {\r\n" - + " \"q\" : \"r\",\r\n" - + " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" - + " }\r\n" - + " }, {\r\n" - + " \"name\" : \"rick\",\r\n" - + " \"parent\" : {\r\n" - + " \"$ref\" : \"#/items/0/parent\"\r\n" - + " },\r\n" - + " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" - + " } ]\r\n" - + "}"; - - Message msg = mapper.readValue(message, Message.class); - System.out.println(msg); + String message = "{\r\n" + " \"items\" : [ {\r\n" + " \"name\" : \"wendy\",\r\n" + " \"parent\" : {\r\n" + + " \"name\" : \"sam\",\r\n" + " \"parent\" : null,\r\n" + " \"props\" : {\r\n" + + " \"s1\" : 1\r\n" + " }\r\n" + " },\r\n" + " \"props\" : {\r\n" + + " \"q\" : \"r\",\r\n" + " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" + " }\r\n" + + " }, {\r\n" + " \"name\" : \"rick\",\r\n" + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/items/0/parent\"\r\n" + " },\r\n" + + " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" + " } ]\r\n" + "}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).parent, msg.items.get(0).props.get("p")); + assertEquals(msg.items.get(0).parent, msg.items.get(1).parent); } } From 3416b688178b494f9c2c110c297bcf1b8385561a Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Tue, 26 May 2026 17:40:39 -0700 Subject: [PATCH 09/10] Refactoring for simplification. --- .../databind/DeserializationContext.java | 17 +++++++++++++++++ .../tools/jackson/databind/ObjectMapper.java | 17 +---------------- .../jackson/databind/ValueDeserializer.java | 15 +++------------ 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/main/java/tools/jackson/databind/DeserializationContext.java b/src/main/java/tools/jackson/databind/DeserializationContext.java index 5526e9d8d0..1c0d430150 100644 --- a/src/main/java/tools/jackson/databind/DeserializationContext.java +++ b/src/main/java/tools/jackson/databind/DeserializationContext.java @@ -2268,6 +2268,23 @@ public DatabindException missingInjectableValueException(String msg, valueId, forProperty, beanInstance); } + // XXX JREF + public List getJRefResolvers() { + @SuppressWarnings("unchecked") + List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); + return jrefs == null?List.of():jrefs; + } + + public void addJRefResolver(JRefResolver resolver) { + @SuppressWarnings("unchecked") + List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); + if (jrefs == null) { + // Lazy creation of JRefResolver list + jrefs = new ArrayList(); + setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, jrefs); + } + jrefs.add(resolver); + } /* /********************************************************************** /* Other internal methods diff --git a/src/main/java/tools/jackson/databind/ObjectMapper.java b/src/main/java/tools/jackson/databind/ObjectMapper.java index 30d519c7a1..3d2cda2ad3 100644 --- a/src/main/java/tools/jackson/databind/ObjectMapper.java +++ b/src/main/java/tools/jackson/databind/ObjectMapper.java @@ -2667,7 +2667,7 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, _findRootDeserializer(ctxt, valueType), null); ctxt.checkUnresolvedObjectId(); // XXX JREF resolve jrefs - resolveJRefs(ctxt, result); + ctxt.getJRefResolvers().forEach(r -> r.resolve(result)); } if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, valueType); @@ -2676,21 +2676,6 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, } } - /** - * Resolve JRefs found during parsing given the root object - * @param ctxt the deserialization context. Must not be null. - * @param root the root object to resolve with. Should not be null. - */ - protected void resolveJRefs(DeserializationContext ctxt, Object root) { - @SuppressWarnings("unchecked") - List jrefs = (List) ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); - if (jrefs != null) { - jrefs.forEach(r -> { - r.resolve(root); - }); - } - } - /** * Similar to {@link #_readMapAndClose} but specialized for JsonNode reading. */ diff --git a/src/main/java/tools/jackson/databind/ValueDeserializer.java b/src/main/java/tools/jackson/databind/ValueDeserializer.java index fed5f25844..d6b052bf0b 100644 --- a/src/main/java/tools/jackson/databind/ValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/ValueDeserializer.java @@ -1,8 +1,6 @@ package tools.jackson.databind; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Set; import tools.jackson.core.JacksonException; @@ -515,24 +513,17 @@ public boolean hasAnySetter() { public void setWithJRef(DeserializationContext ctxt, Object value, SetterFunction f) throws RuntimeException { // If the value (result of deserialization) is a JRefPath if (value instanceof JRefPath) { - @SuppressWarnings("unchecked") - List jrefs = (List) ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); - if (jrefs == null) { - // Lazy creation of JRefResolver list - jrefs = new ArrayList(); - ctxt.setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, jrefs); - } // Add this new resolver to the jrefs attribute (List). // The JRefResolver resolve method is later called to actually // resolve, given the root object in the object graph - jrefs.add(new JRefResolver((JRefPath) value, f)); - } else { + ctxt.addJRefResolver(new JRefResolver((JRefPath) value,f)); + } else { // We simply call the given SetterFunction to set with the value param try { f.set(value); } catch (Throwable e) { if (e instanceof RuntimeException) throw (RuntimeException) e; - else throw new RuntimeException(e); + else throw new RuntimeException("Throwable in SetterFunction.f",e); } } } From f8e8a4d510f79046a1157cd1b81522c28504a003 Mon Sep 17 00:00:00 2001 From: Scott Lewis Date: Thu, 28 May 2026 13:46:38 -0700 Subject: [PATCH 10/10] Refactoring as per discussion on pr #6014 https://github.com/FasterXML/jackson-databind/pull/6014#issuecomment-4556987814 1) Removed/deleted ValueDeserializer.setWithJRef method. 2) Replaced usage of ValueDeserializer.setWithJRef in a) MethodProperty b) MapDeserializer c) CollectionDeserializer d) StringCollectionDeserializer 3) Added support methods in DeserializationContext, ValueDeserializer 4) Added ObjectMapper._deserializationContext() code to look for registered JRefModules upon DeserializationContextExt creation and if found to call DeserializationContext.initializeJRefProcessing() --- .../databind/DeserializationContext.java | 32 ++++++++++++----- .../tools/jackson/databind/ObjectMapper.java | 8 ++++- .../jackson/databind/ValueDeserializer.java | 36 ++++--------------- .../databind/deser/impl/MethodProperty.java | 29 ++++++++------- .../deser/jdk/CollectionDeserializer.java | 17 ++++++--- .../databind/deser/jdk/MapDeserializer.java | 27 ++++++++++---- .../jdk/StringCollectionDeserializer.java | 18 +++++++--- 7 files changed, 98 insertions(+), 69 deletions(-) diff --git a/src/main/java/tools/jackson/databind/DeserializationContext.java b/src/main/java/tools/jackson/databind/DeserializationContext.java index 1c0d430150..5b4112c3b4 100644 --- a/src/main/java/tools/jackson/databind/DeserializationContext.java +++ b/src/main/java/tools/jackson/databind/DeserializationContext.java @@ -2268,23 +2268,39 @@ public DatabindException missingInjectableValueException(String msg, valueId, forProperty, beanInstance); } - // XXX JREF + // XXX JREF handling start + public void enableJRefProcessing() { + setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, new ArrayList()); + } + + public boolean isJRefProcessingEnabled() { + return getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null; + } + public List getJRefResolvers() { @SuppressWarnings("unchecked") List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); return jrefs == null?List.of():jrefs; } - public void addJRefResolver(JRefResolver resolver) { + public boolean addJRefResolver(JRefResolver resolver) { @SuppressWarnings("unchecked") List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); - if (jrefs == null) { - // Lazy creation of JRefResolver list - jrefs = new ArrayList(); - setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, jrefs); - } - jrefs.add(resolver); + if (jrefs != null) { + // If jrefs list is not set then ignore + return jrefs.add(resolver); + } + return false; } + + public JRefPath findJRefPathForValue(Object value) { + if (getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null && value instanceof JRefPath) { + return (JRefPath) value; + } else { + return null; + } + } + // XXX JREF handling end /* /********************************************************************** /* Other internal methods diff --git a/src/main/java/tools/jackson/databind/ObjectMapper.java b/src/main/java/tools/jackson/databind/ObjectMapper.java index 3d2cda2ad3..54c369f766 100644 --- a/src/main/java/tools/jackson/databind/ObjectMapper.java +++ b/src/main/java/tools/jackson/databind/ObjectMapper.java @@ -2727,8 +2727,14 @@ protected DeserializationContextExt _deserializationContext(JsonParser p) { // NOTE: only public to allow for testing public DeserializationContextExt _deserializationContext() { - return _deserializationContexts.createContext(deserializationConfig(), + DeserializationContextExt result = _deserializationContexts.createContext(deserializationConfig(), /* FormatSchema */ null, _injectableValues); + for(JacksonModule m: registeredModules()) { + if (m instanceof JRefModule) { + result.enableJRefProcessing(); + } + } + return result; } // 15-Feb-2026, tatu: Unused by databind itself diff --git a/src/main/java/tools/jackson/databind/ValueDeserializer.java b/src/main/java/tools/jackson/databind/ValueDeserializer.java index d6b052bf0b..554cbc2d28 100644 --- a/src/main/java/tools/jackson/databind/ValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/ValueDeserializer.java @@ -498,37 +498,15 @@ public boolean hasAnySetter() { return false; } - // XXX JREF New method for supporting JREF for deserialization - /** - * Set the value with the - * @param ctxt the deserialization context. Must not be null. - * @param value the value to use to set. If an instanceof JRefPath, - * then a JRefResolver is created, and calling the SetterFunction is deferred until after the root - * has been returned. If not JRefPath, then the SetterFunction set is called - * immediately with the value Object. - * @param f the SetterFunction to use to set to the given value. - * @throws RuntimeException if JRefResolver cannot be created, or the - * SetterFunction.set throws a RuntimeException - */ - public void setWithJRef(DeserializationContext ctxt, Object value, SetterFunction f) throws RuntimeException { - // If the value (result of deserialization) is a JRefPath - if (value instanceof JRefPath) { - // Add this new resolver to the jrefs attribute (List). - // The JRefResolver resolve method is later called to actually - // resolve, given the root object in the object graph - ctxt.addJRefResolver(new JRefResolver((JRefPath) value,f)); - } else { - // We simply call the given SetterFunction to set with the value param - try { - f.set(value); - } catch (Throwable e) { - if (e instanceof RuntimeException) throw (RuntimeException) e; - else throw new RuntimeException("Throwable in SetterFunction.f",e); - } - } + // XXX JREF support method for subclasses + public JRefPath findJRefPathFromValue(DeserializationContext ctxt, Object value) { + if (ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null && value instanceof JRefPath) { + return (JRefPath) value; + } else { + return null; + } } - /* /********************************************************************** /* Helper classes diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index e7faabe109..eaad0c58e2 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -4,8 +4,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.ArrayList; -import java.util.List; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; @@ -13,7 +11,6 @@ import tools.jackson.databind.*; import tools.jackson.databind.deser.NullValueProvider; import tools.jackson.databind.deser.SettableBeanProperty; -import tools.jackson.databind.deser.std.StdDeserializer; import tools.jackson.databind.introspect.*; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.util.Annotations; @@ -132,21 +129,23 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - // XXX JREF handling. ValueDeserializer.setWithJRef - // checks the return value, and if type JRefPath - // it defers calling the provided SetterFunction until - // the value is resolved (after root deserialization complete). - // if value is not of type JRefPath, then the given setter function - // is called immediately with the given value - _valueDeserializer.setWithJRef(ctxt, value, (v) -> { + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise returns null + JRefPath jrefPath = _valueDeserializer.findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + // This is only called if JRefPath is returned from find method above + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + return setAndReturn(ctxt, instance, v); + })); + } else { try { - _setter.get().invokeExact(instance, v); + _setter.get().invokeExact(instance, value); } catch (Throwable e) { - _throwAsJacksonE(p, e, v); + _throwAsJacksonE(p, e, value); } - return instance; - }); - + } } @Override diff --git a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java index 19368af051..9491404189 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java @@ -399,10 +399,19 @@ protected Collection _deserializeFromArray(JsonParser p, Deserialization } } // XXX JREF handling - setWithJRef(ctxt, value, (v) -> { - result.add(v); - return result; - }); + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + result.add(v); + return result; + })); + } else { + // do whatever was done before JRefPath + result.add(value); + } /* 17-Dec-2017, tatu: should not occur at this level... } catch (UnresolvedForwardReference reference) { diff --git a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java index eac51fb2f9..83e4b16950 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java @@ -20,6 +20,8 @@ import tools.jackson.databind.BeanProperty; import tools.jackson.databind.DeserializationConfig; import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JRefPath; +import tools.jackson.databind.JRefResolver; import tools.jackson.databind.JavaType; import tools.jackson.databind.KeyDeserializer; import tools.jackson.databind.MapperFeature; @@ -689,15 +691,26 @@ protected final Map _readAndBindStringKeyMap(JsonParser p, Deseri if (useObjectId) { referringAccumulator.put(key, value); } else { - // XXX JREF - final String k = key; - this.setWithJRef(ctxt, value, (v) -> { - Object oldValue = result.put(k, v); + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + final String k = key; + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + Object oldValue = result.put(k, v); + if (oldValue != null) { + _squashDups(ctxt, result, k, oldValue, v); + } + return result; + })); + } else { + Object oldValue = result.put(key, value); if (oldValue != null) { - _squashDups(ctxt, result, k, oldValue, v); + _squashDups(ctxt, result, key, oldValue, value); } - return result; - }); + } } } catch (UnresolvedForwardReference reference) { handleUnresolvedReference(ctxt, referringAccumulator, key, reference); diff --git a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java index 6fc0a01c7c..b13a9560d9 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java @@ -313,11 +313,19 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC continue; } } - // XXX JREF - setWithJRef(ctxt, value, (v) -> { - result.add((String) v); - return result; - }); + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + result.add((String) v); + return result; + })); + } else { + result.add((String) value); + } } } catch (Exception e) { throw DatabindException.wrapWithPath(ctxt, e,