Use and effects of @JsonIdentityInfo for recursive json schema definitions #311
Replies: 13 comments 57 replies
-
|
Without knowing full details/intents I could be wrong, but I think use of Also quick note on "in practice": that's clumsy way of saying "this is how Jackson implements it" (but implying other implementations could produce output that is not necessarily ordered this way). |
Beta Was this translation helpful? Give feedback.
-
|
@cowtowncoder I've just discovered the JavaScript Object Graph project: https://github.com/jsog/jsog Interestingly, it has a Jackson plugin implementation https://github.com/jsog/jsog-jackson that uses the @JsonIdentityInfo generator property to customize the generator. I'm looking over this code myself, but wanted to make you aware of it's existence too. Looks like it was produced a while ago, so I'm hoping it still functions as described with recent Jackson. |
Beta Was this translation helpful? Give feedback.
-
|
Hi Tatu. Over the holidays, I had some q and a with folks involved with json schema specification (i.e. JRef and hyper jump) https://github.com/hyperjump-io/browser The JRef spec is based upon Json pointers and adds a json reference type (and mime type for document), that is then serialized to (e.g.): { "$ref": "#/path/to/object/in/current/doc" }. I learned that the complete JRef spec (and hyperjump browser as example) extends the JSON pointer syntax to include an arbitrary uri...e.g. { "$ref": "https://foo.bar/path/to/doc#/path/to/object/in/doc" }. As you can see, however, if the uri fragment only is used (i.e '#'...with the json pointer syntax), that the current document is assumed. After working with Jason a bit, he's written a 'fragment (#)/current doc-supporting only' typescript implementation of serialization and validation. Given his ts impl, I've created a python impl and have now got it working for the python impl of the MCP sdk (pydantic as json ser/parse lib)...specifically for serializing object graphs without sending multiple copies of referenced objects (as JsonIdentityInfo does). One utility of this...like JsonIdentityInfo, is to serialize object graphs without having to send multiple copies of object data when a reference will do. Because of the pointer specification...$ref name and specified value syntax...it works same in all languages, and without any additional json-schema-specific standardization. I was thinking of now creating Jackson-based Java impl and contributing it to Jackson, but thought that it might be better to discuss with you before doing so. Now that I've done it in Python/pydantic...it's very short...I believe it would be easy to produce in Java or most other languages, really. I have the typescript/javascript impls, along with the python impl and can provide them to you. I'm working out with Jason how he wants to distribute this 'reduced/fragment-only' impls of JRef (not full URI support...which obviously implies client->server (http/https) communication for references). Since this functionality is all about interoperability, it makes most sense (IMHO) to make freely available for all (specification + impl). |
Beta Was this translation helpful? Give feedback.
-
|
On Tue, Apr 14, 2026, 4:19 PM Tatu Saloranta ***@***.***> wrote:
Jackson does all Object graph traversal (introspection) on its own, and
that cannot really be replaced.
I'm not suggesting replacement, Im suggesting enhancement...perhaps with
new options to enable/support json ptrs or not.
If support for json ptrs (rfc 6901) were standardized....across languages
and libs...wouldn't that warrant a Jackson enhancement to support?
I don't understand the hesitation.
I don't really have a recipe to give unfortunately.
…
I'd almost suggest that If it doen't fit within @JsonIdentityInfo
framework, I am not sure it belongs inside Jackson at all. Perhaps it
should be an external companion library or something.
—
Reply to this email directly, view it on GitHub
<#311 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABHB5BZUAP2BXC4MQLR6SD4V3BQ3AVCNFSM6AAAAACJ557PEWVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTMNJWGI3TOMA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
|
On Fri, Apr 24, 2026, 10:33 AM Tatu Saloranta ***@***.***> wrote:
You cannot just add custom serializer(s) (and of course matching
deserializer(s)) for such cross-cutting concern, unless support is also via
some custom type or types (one(s) like JsonNode). Ideally this would
integrate with other functionality.
But if not, then something else needs to be designed. What that would be,
I do not know at this point.
I find it difficult to believe that Jackson can't support this without a
rewrite.
ObjectMapper?.
|
Beta Was this translation helpful? Give feedback.
-
FYI: google/gson#1443 Seems like maybe ObjectMapper might be a better place to start? |
Beta Was this translation helpful? Give feedback.
-
|
Created a new branch of jackson-databind here. Here's the diff I added a new MapperFeature.USE_JREF, very simple changes to ObjectMapper.writeValueAsString and readValue(String,Class) added the current jref_java class, and created a simple JsonJRefTest. There are two test methods in JsonJRefTest: testBuildAndResolveRefs which only uses JRef.buildRefs and JRef.resolveRefs (no ObjectMapper) and testObjectMapperJRef which obviously does use the enhanced JRef for buildRefs (called prior to jackson serialization), and resolveRefs (called after jackson deserialization). Here's the output of the testBuildAndResolveTests (JRef only): ---testBuildAndResolveRefs
message=Message[items=[Human[name=wendy, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}], Human[name=rick, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}]]]
jrefSerResult={items=[{name=wendy, parent={name=sam, parent=null, props={s1=1}}, props={p={$ref=#/items/0/parent}, q=r}}, {name=rick, parent={$ref=#/items/0/parent}, props={$ref=#/items/0/props}}]}
resolveRefResult={items=[{name=wendy, parent={name=sam, parent=null, props={s1=1}}, props={p={name=sam, parent=null, props={s1=1}}, q=r}}, {name=rick, parent={name=sam, parent=null, props={s1=1}}, props={p={name=sam, parent=null, props={s1=1}}, q=r}}]}The Human and Mess types exist in the original message, the jrefSerResult creates/substitutes the {"$ref":"#/path/to/ref"} references...after the first one, and the resolveRefResult resolves the references. Obviously the jref resolve produces Map<String,Object> types rather than the original types, since there's no type information in the json. Resolve refs does it's job. Now for the jref enhanced object mapper output. ---testObjectMapperJRef
message=Message[items=[Human[name=wendy, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}], Human[name=rick, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}]]]
serializedMessage={
"items" : [ {
"name" : "wendy",
"parent" : {
"name" : "sam",
"parent" : null,
"props" : {
"s1" : 1
}
},
"props" : {
"p" : {
"$ref" : "#/items/0/parent"
},
"q" : "r"
}
}, {
"name" : "rick",
"parent" : {
"$ref" : "#/items/0/parent"
},
"props" : {
"$ref" : "#/items/0/props"
}
} ]
}
deserializedMessage=Message[items=[Human[name=wendy, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}], Human[name=rick, parent=Human[name=null, parent=null, props=null], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}]]]The message is same as jref only...just the printed out object graph. The serializedMessage also looks right to me...as all > 1 internal references are replaced with (e.g.): { "$ref" : "#/items/0/parent" } via the call to JRef.buildRefs(result). The deserializedMessage (after Jackson deserialize + JRef.resolveRefs called) looks mostly right deserializedMessage=Message[items=[Human[name=wendy, parent=Human[name=sam, parent=null, props={s1=1}], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}], Human[name=rick, parent=Human[name=null, parent=null, props=null], props={p=Human[name=sam, parent=null, props={s1=1}], q=r}]]]All the references were resolved properly (to sam) except for the rick.parent reference: Human[name=rick, parent=Human[name=null, parent=null, props=null]The human name, parent, and props are null...i.e. the JRef resolve did not result in pointing to Human[name=sam...] This is in contrast to the JRef-only test...where the resolveRefs is done fine (even though the type of the resulting reference is Map). I looked in the debugger and found
Just before calling JRef.resolveRefs, the Object result from _readMapAndClose(...) has the rick.parent reference set to object of type Human, but with all fields set to null. The string input to readValue(String,Class) has this as it's rick.parent json value: "parent" : {
"$ref" : "#/items/0/parent"
},As you can see from the serializedMessage value. But unlike the other references in the readValue input string, rick.parent somehow was set during Jackson deserialization to a pojo of Human type, but with the fields set to null. This is puzzling to me as I can't explain how the parent json ptr got somehow resolved to an empty Human instance during deserialization when none of the others had this (they come out of jackson deserialization as a LinkedHashMap with entry.key="$ref" and value="#/whatever". Does this make deserialization behavior make sense to you? |
Beta Was this translation helpful? Give feedback.
-
|
I've added the following test method @Test
void testDeserializationOnly() throws Exception {
String serMessage = "{\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"
+ " \"p\" : {\r\n"
+ " \"$ref\" : \"#/items/0/parent\"\r\n"
+ " },\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"
+ " \"props\" : {\r\n"
+ " \"$ref\" : \"#/items/0/props\"\r\n"
+ " }\r\n"
+ " } ]\r\n"
+ "}";
// Use MAPPER / no JRef
Object value = MAPPER.readValue(serMessage, Message.class);
System.out.println(value);
}
}The MAPPER is a plain ol ObjectMapper...i.e. not the JRef-enhanced one. In the string input this is the value for rick.parent "parent" : {
"$ref" : "#/items/0/parent"
},After deserialization the above rick.parent value is: Human[name=null, parent=null, props=null] I presume what's happening is that the deserialization is expecting type Human because that's the type of the parent property in the enclosing object. Is this where mapper.readTree or something else would come in? Perhaps that you are right that such standardization just can't be a feature of Jackson json serialization. Apologies for my persistence. |
Beta Was this translation helpful? Give feedback.
-
|
I've been looking at the tools.jackson.databind.objectid.JSOGDeserialize622Test and have landed upon the following strategy. Please let me know if this can't work: Like the test, create a new type called JRef (analogous to JSOGRef in test code). static class JRef
{
@JsonProperty(REF_KEY)
public String ref;
public JRef() { }
public JRef(String val) {
ref = val;
}
@Override
public String toString() { return "[JRef="+ref+"]"; }
@Override
public int hashCode() {
return ref.hashCode();
}
@Override
public boolean equals(Object other) {
return (other instanceof JRef)
&& ((JRef) other).ref == this.ref;
}
}Then create a deserializer for this type static class JRefDeserializer extends ValueDeserializer<JRef>
{
@Override
public JRef deserialize(JsonParser p, DeserializationContext ctxt)
{
JsonNode node = ctxt.readTree(p);
if (node.isString()) {
return new JRef(node.asString());
}
JsonNode n = node.get(REF_KEY);
if (n == null) {
ctxt.reportInputMismatch(JRef.class, "Could not find key '"+REF_KEY
+"' from ("+node.getClass().getName()+"): "+node);
}
return new JRef(n.asString());
}
}and call addDeserializers in JRef module with this type static class JRefDeserializerResolver implements Deserializers {
@Override
public ValueDeserializer<?> findBeanDeserializer(JavaType type,
DeserializationConfig config, BeanDescription.Supplier beanDescRef)
{
if (!JRef.class.isAssignableFrom(type.getRawClass())) {
return null;
}
return new JRefDeserializer();
}
@Override
public boolean hasDeserializerFor(DeserializationConfig config,
Class<?> valueType) {
return false;
}
}I notice, however, that the JRefDeserializerResolver is never consulted upon readValue, apparently because the type is never referred to statically in the java classes, but rather is by default handled by the Map serializer (because jreferences have form { "$ref": string } Is there some way to hook into the runtime default deserializer lookup....so that the maps with a single entry with key == "$ref" can be deserialized into JRef? AFAICT the JsonIdentityInfo annotation...which triggers using references is all done statically/contextually based upon the return types rather than based upon the actual value in the input stream (defaulting to map). Or perhaps I should be looking to create some reference type instead of JRef during deserialization so that the reference can be resolved using existing reference handling logic (e.g. to handle forward references)? Thanksinadvance. |
Beta Was this translation helpful? Give feedback.
-
|
I'm thinking of taking the strategy of modifying BeanDeserializer and all it's property's ValueDeserializers on construction/contextualization so that they all can be referred to during deserialization via a Map with one key='$ref' and value of path string...e.g. '{ "$ref": "#/path/to/local/instance" }'. The idea is to have every BeanDeserializer be dynamically setup as if it was defined statically with a JsonIdentityInfo declaration. I've been debugging through the JsonIdentityInfo annotation processor to get hints about how/when this is all setup. If there are tests or other code that can/could help with that understanding I would appreciate pointers. |
Beta Was this translation helpful? Give feedback.
-
|
Quick note: instead of directly registering deserializers capable of resolving jrefs, you probably want/need But you do want/need to wrap EVERY deserialize this way since target type can be anything. |
Beta Was this translation helpful? Give feedback.
-
|
Signs of life! New branch json_deserialization See JRef* classes in tools.jackson.databind package JRefModule a SimpleModule that implements ValueDeserializerModifier to create JRefValueDeserializer instances to wrap all value deserializers. JRefValueDeserializer.deserialize method uses the ctxt.readTree and inspects the JsonNode for { "$ref", "#/path/to/object" } (JRefs) and if found returns a JRefResolver instance with the deserialization context and path. Currently, I've only handled the returned JRefResolver instance in the BeanDeserializer MethodProperty here. What this does is call JRefResolver.setSetter(MethodProperty,Object) to later be used in resolve(Object root). Obviously this only is appropriate for BeanDeserializer/MethodProperty, and so the JRefResolver structure has to be generalized so that it can do the appropriate value setting for all types of ValueDeserializers (maps, lists, objects, etc). But for now the JRefBeanSerializerTest code input string only has jrefs (2 of them) that are both associated with bean types/BeanValueDeserializers. The MethodProperty impl calls the JRefResolver.setSetter and then adds the JRefResolver to a list of JRefResolver attached to the BeanDeserializationContext instance as an attribute named "jrefs". Using a context attribute may not be what you want to do...I just used it as a way to associate JRefResolver instances with a single context. There may be a better/cleaner way to do so (e.g. adding list of jref resolvers to context). After the root object value is returned by ObjectMapper._readRootValue each JRefResolver's resolve(Object root) method is called, and the JRefResolver.resolve(root) uses the jref local path and some JRef code to lookup the root object graph and get the referenced object. Finally the JRefResolve.resolve(root) calls the methodProperty.set method to actually set the bean property to the referenced object value. This only works with bean properties, and JRefResolver needs to be able to handle JRefResolver instances (created by JRefValueDeserializer) when returned in all other contexts. I'm not sure how best this is to be done...e.g. by adding methods to StdSerializer and subclasses or generalizing setSetter (e.g. providing a function). There also might be some way to reuse what's already present in Jackson to handle all the different types of valuedeserializers in a cleaner way. |
Beta Was this translation helpful? Give feedback.
-
|
@cowtowncoder as per the previous posting/second paragraph, I'm now trying to understand how best to setup the JRefResolver with an appropriate function that can be called during JRefResolver.resolve(root) so that the resolve uses the ctxt, path to lookup on root the first object, which according to jref local contract has a deserialized version of the jref referenced object of any java type. What is currently provided by JRefResolver.setSetter is (first arg) the MethodProperty and (second arg) the target object...i.e. that will have the target method property set called with the value found during JRefResolver lookup on root. I'm trying to generalize this so that it will work on any type of ValueDeserializer/StdValueDeserializer instead of just BeanDeserializer as the impl does now. SettableBeanProperty is superclass of MethodProperty. Would it be possible to dynamically construct an appropriate SettableBeanProperty subclass...e.g for a MapDeserializer that simply implements set() calls/sets via target.put(name,value). Maybe a new protected methods on StdDeserializer for handling JRefs/JRefResolvers by a) setting the path, deserialization ctxt upon construction in JRefValueDeserializer.deserialize/2. Does this (new subtypes of SettableBeanProperty seem like a good way to go to handle all types of ValueDeserializers like Map, List, Object...anything that can a jref can refer to and later resolved? in addition to BeanValueDeserializers? Or is there a better way of calling a setter on any deserialized object? |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
I'm a little new to the Jackson API, although very experienced in Java.
I'm working on a proposal for adding Groups to the MCP protocol. The mcp-java-sdk uses Jackson (2.19.something I think).
In the Groups proposal I've suggested introducing a nested Group structure, by adding a 'parent' Group attribute. This provides for arbitrary hierarchy of groups.
With normal serialization/deserialization of json with this schema the Group.parent recursive/nested structure could produce a lot of duplication of groups (for example if the same nested hierarchy of Groups was assigned to multiple Tools).
I've heard that the @JsonIdentityInfo annotation can be used for this situation, to replace all but the first given Group instance with a json reference, so that on deserialization the Group tree structure is reconstituted by clients.
I've read the docs for @JsonIdentityInfo and it says this:
...In practice this is done by serializing the first instance as full object and object identity, and other references to the object as reference values....
My question is: Is adding @JsonIdentityInfo to the Group type...e.g. here all that's necessary to get this 'in practice' behavior? (i.e. serialize to json the first reference to a given Group, and send a json reference for all other references?). Or are there other options required for getting this in practice behavior (and/or changing/customizing that behavior)?
Beta Was this translation helpful? Give feedback.
All reactions