Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
06c5bcb
Initial checkin of JRefValueDeserializer/JRefResolver
scottslewis May 20, 2026
2e59cee
Added JRefSetter and JRefSetterFunction types, simplified JRefPath and
scottslewis May 21, 2026
d7e2e6b
Removed JRefSetter and JRefSetterFunction in favor of new SetterFunction
scottslewis May 21, 2026
805fc1b
Added support in ValueDeserializerModifier for using
scottslewis May 22, 2026
962e0f2
Merge branch 'FasterXML:3.x' into jref_deserialization
scottslewis May 22, 2026
36ef70c
Added jref handling to StringCollectionDeserializer. Added test case
scottslewis May 24, 2026
fbc2d63
Added test cases for Double, Float, Boolean, and String handling in
scottslewis May 24, 2026
d038844
Added more (passing) test cases
scottslewis May 25, 2026
5fd7a24
Refactored for simplicity. Added string constants for "$ref" and
scottslewis May 25, 2026
dfe1eb2
Merge branch '3.x' into jref_deserialization
scottslewis May 26, 2026
3416b68
Refactoring for simplification.
scottslewis May 27, 2026
42efbaa
Merge branch 'jref_deserialization' of https://github.com/scottslewis…
scottslewis May 27, 2026
3854fe9
Merge branch '3.x' into jref_deserialization
scottslewis May 27, 2026
f8e8a4d
Refactoring as per discussion on pr #6014
scottslewis May 28, 2026
8589262
Merge branch 'jref_deserialization' of https://github.com/scottslewis…
scottslewis May 28, 2026
203ad96
Merge branch '3.x' into jref_deserialization
scottslewis May 28, 2026
ac4ea69
Merge branch '3.x' into jref_deserialization
scottslewis May 28, 2026
4746244
Merge branch '3.x' into jref_deserialization
cowtowncoder May 29, 2026
cfe2bbf
Fix for comment
scottslewis May 30, 2026
106a93a
Fix for comment
scottslewis May 30, 2026
26f93d9
Changed JRefResolveException into DatabindException subclass (rather
scottslewis May 30, 2026
99ed93c
Merge branch 'jref_deserialization' of https://github.com/scottslewis…
scottslewis May 30, 2026
9f4b06f
API simplification
scottslewis May 30, 2026
63b6646
Added jref tests package. Created JRefBeanPublicMemberDeserializerTest
scottslewis Jun 1, 2026
52b1ddb
Added JRefSerializer class to build jrefs from original OG
scottslewis Jun 1, 2026
b0c4a3d
Backup of node work
scottslewis Jun 6, 2026
76f8765
Added JRefValueSerializerModifier and appropriate JRefValueSerializer
scottslewis Jun 6, 2026
6e7f70b
Merge branch '3.x' into jref_deserialization
scottslewis Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/main/java/tools/jackson/databind/DeserializationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,38 @@ public DatabindException missingInjectableValueException(String msg,
valueId, forProperty, beanInstance);
}

// XXX JREF handling start
public void enableJRefProcessing() {
setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, new ArrayList<JRefResolver>());
}

public boolean addJRefResolver(JRefResolver resolver) {
@SuppressWarnings("unchecked")
List<JRefResolver> jrefs = (List<JRefResolver>) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR);
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;
}
}

public void resolveJRefs(Object root) throws JacksonException {
@SuppressWarnings("unchecked")
List<JRefResolver> resolvers = (List<JRefResolver>) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR);
if (resolvers != null) {
resolvers.forEach(r -> r.resolve(this, root));
}
}

// XXX JREF handling end
/*
/**********************************************************************
/* Other internal methods
Expand Down
300 changes: 300 additions & 0 deletions src/main/java/tools/jackson/databind/JRefModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
package tools.jackson.databind;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonPointer;
import tools.jackson.core.JsonToken;
import tools.jackson.core.TokenStreamContext;
import tools.jackson.databind.BeanDescription.Supplier;
import tools.jackson.databind.deser.ValueDeserializerModifier;
import tools.jackson.databind.deser.std.DelegatingDeserializer;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.TreeTraversingParser;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.databind.ser.std.DelegatingSerializer;
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;

public class JRefModule extends SimpleModule {

private static final long serialVersionUID = 1L;

public JRefModule() {
super("JRefModule");
}

@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addDeserializerModifier(new JRefValueDeserializerModifier());
context.addSerializerModifier(new JRefValueSerializerModifier());
}

@FunctionalInterface
interface Serializer {
void serialize() throws RuntimeException;
}

public class JRefValueSerializerModifier extends ValueSerializerModifier {

private static final long serialVersionUID = 1L;

static final String PTR_MAP_ATTR = JRefValueSerializerModifier.class.getName() + ".ptrMap";

public class JRefValueSerializer extends DelegatingSerializer {

protected JRefValueSerializer(ValueSerializer<?> delegatee) {
super(delegatee);
}

@Override
public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property) {
// If the serialization context doesn't already have it, a new Integer->JsonPointer
// map is set to PTR_MAP_ATTRIBUTE for lookup and addition of serialized values
if (ctxt.getAttribute(PTR_MAP_ATTR) == null) {
ctxt.setAttribute(PTR_MAP_ATTR, new ConcurrentHashMap<>());
}
return super.createContextual(ctxt, property);
}

@SuppressWarnings("unchecked")
protected JsonPointer findJsonPointer(Object value, SerializationContext ctxt) {
if (value != null) {
return ((Map<Integer,JsonPointer>) ctxt.getAttribute(PTR_MAP_ATTR)).get(System.identityHashCode(value));
}
return null;
}

@SuppressWarnings("unchecked")
protected void addJsonPointerIfComplete(Object value, JsonGenerator gen, SerializationContext ctxt) {
if (value != null) {
TokenStreamContext writeContext = gen.streamWriteContext();
if (writeContext.hasPathSegment()) {
((Map<Integer,JsonPointer>) ctxt.getAttribute(PTR_MAP_ATTR)).put(System.identityHashCode(value), JsonPointer.forPath(writeContext, false));
}
}
}

protected void serializeWithJRef(Object value, JsonGenerator gen, SerializationContext ctxt, Serializer serializer) {
// First look for json pointer for instance
JsonPointer foundPtr = findJsonPointer(value, ctxt);
if (foundPtr != null) {
// If found, write out and we're done!
gen.writeStartObject();
gen.writeStringProperty(JRefPath.JREF_REF, JRefUtil.HASH + foundPtr.toString());
gen.writeEndObject();
} else {
// Call the given serializer to do it's work (with typeref or not)
serializer.serialize();
// Add JsonPointer to map if the streamWriteContext has a path segment to contribute
addJsonPointerIfComplete(value, gen, ctxt);
}
}

@Override
public void serializeWithType(Object value, JsonGenerator gen, SerializationContext ctxt,
TypeSerializer typeSer) {
serializeWithJRef(value, gen, ctxt, () -> super.serializeWithType(value, gen, ctxt, typeSer));
}

@Override
public void serialize(Object value, JsonGenerator gen, SerializationContext ctxt) {
serializeWithJRef(value, gen, ctxt, () -> super.serialize(value, gen, ctxt));
}

@Override
public ValueSerializer<Object> newDelegatingInstance(ValueSerializer<?> delegatee) {
return new JRefValueSerializer(delegatee);
}

}

@Override
public ValueSerializer<?> modifySerializer(SerializationConfig config, Supplier beanDesc,
ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyArraySerializer(SerializationConfig config, ArrayType valueType,
Supplier beanDesc, ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyCollectionSerializer(SerializationConfig config, CollectionType valueType,
Supplier beanDesc, ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyCollectionLikeSerializer(SerializationConfig config,
CollectionLikeType valueType, Supplier beanDesc, ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyMapSerializer(SerializationConfig config, MapType valueType, Supplier beanDesc,
ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyMapLikeSerializer(SerializationConfig config, MapLikeType valueType,
Supplier beanDesc, ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyEnumSerializer(SerializationConfig config, JavaType valueType,
Supplier beanDesc, ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

@Override
public ValueSerializer<?> modifyKeySerializer(SerializationConfig config, JavaType valueType, Supplier beanDesc,
ValueSerializer<?> serializer) {
return new JRefValueSerializer(serializer);
}

}

public class JRefValueDeserializerModifier extends ValueDeserializerModifier {

private static final long serialVersionUID = 1L;

@FunctionalInterface
interface Deserializer{
Object deserialize(JsonParser p);
}

public class JRefValueDeserializer extends DelegatingDeserializer {

protected 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;
}

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;
}

protected Object deserializerWithJRef(JsonParser p, DeserializationContext ctxt,
TypeDeserializer typeDeserializer, Deserializer deserializer) {
JRefFindResult findResult = findJRef(p, ctxt, typeDeserializer);
if (findResult.jrefPath != null) {
return findResult.jrefPath;
} else {
return deserializer.deserialize(findResult.parser);
}
}


@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);
}

}


@Override
public ValueDeserializer<?> modifyArrayDeserializer(DeserializationConfig config, ArrayType valueType,
Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyCollectionDeserializer(DeserializationConfig config, CollectionType type,
Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type,
Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyCollectionLikeDeserializer(DeserializationConfig config,
CollectionLikeType type, Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyDeserializer(DeserializationConfig config, Supplier beanDescRef,
ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyMapDeserializer(DeserializationConfig config, MapType type,
Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

@Override
public ValueDeserializer<?> modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type,
Supplier beanDescRef, ValueDeserializer<?> deserializer) {
return new JRefValueDeserializer(deserializer);
}

}

}
52 changes: 52 additions & 0 deletions src/main/java/tools/jackson/databind/JRefPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package tools.jackson.databind;

import java.util.Objects;

import tools.jackson.databind.jsontype.TypeDeserializer;

public class JRefPath {

public static final String JREF_REF = "$ref";
private final DeserializationContext ctxt;
private final String path;
private final 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;
}

public ValueDeserializer<?> getValueDeserializer() {
return deserializer;
}

public TypeDeserializer getTypeDeserializer() {
return typeDeserializer;
}

@Override
public String toString() {
return "JRefPath[ctxt=" + ctxt + ", path=" + path + ", deserializer=" + deserializer + ", typeDeserializer="
+ typeDeserializer + "]";
}

}
Loading