From 87d4ad90d7f40badd0f0f5524be307c02bc36068 Mon Sep 17 00:00:00 2001 From: gus Date: Thu, 26 Jun 2025 21:47:27 -0400 Subject: [PATCH 01/11] Half of a solution for issue #26 - still haven't Figured out how to add the type information to JSOGRef as shown in Issue26Test.java --- .../jackson/jsog/JSOGRefDeserializer.java | 30 ++++++- .../voodoodyne/jackson/jsog/Issue26Test.java | 88 +++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java index fd320b9..fc32113 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java @@ -1,12 +1,13 @@ package com.voodoodyne.jackson.jsog; +import java.io.IOException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; -import java.io.IOException; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; /** * Knows how to take either form of a JSOGRef (string or {@ref:string} and convert it back into a JSOGRef. @@ -16,7 +17,7 @@ public class JSOGRefDeserializer extends JsonDeserializer { @Override - public JSOGRef deserialize(JsonParser jp, DeserializationContext ctx) throws IOException, JsonProcessingException { + public JSOGRef deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { JsonNode node = ctx.readValue(jp, JsonNode.class); if (node.isTextual()) { return new JSOGRef(node.asText()); @@ -25,4 +26,27 @@ public JSOGRef deserialize(JsonParser jp, DeserializationContext ctx) throws IOE } } + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { + // in the event of default typing, this gets called instead of deserialize above, and it seems to get + // called for each @id (the first case) and for each {"@ref":"#"} (the second case) + if (p.currentToken() == JsonToken.VALUE_STRING) { + return new JSOGRef(p.getText()); + } + if (p.currentToken() == JsonToken.FIELD_NAME) { + // we are being called for { "@ref":"#" } + // and starting here ------^ + p.nextToken(); + if (p.currentToken() != JsonToken.VALUE_STRING) { + throw new IllegalStateException("@ref attribute should be followed by a value?"); + } + String text = p.getText(); //grab our id + p.nextToken(); // consume END_OBJECT + return new JSOGRef(text); + } else { + throw new IllegalStateException("Unexpected Token Type:" + p.currentToken().name()); + } + } } + + diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java new file mode 100644 index 0000000..c7f3cb7 --- /dev/null +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -0,0 +1,88 @@ +package com.voodoodyne.jackson.jsog; + +import java.util.ArrayList; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import org.junit.Test; + +public class Issue26Test { + + // @formatter:off + String TEST_JSON="[\"java.util.ArrayList\",[" + + + "{" + + "\"@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Outer\"," + + "\"@id\":\"1\"," + + "\"inner\":{" + + "\"@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Inner\"," + + "\"@id\":\"2\"," + + "\"outer\":{" + + "\"@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Outer\"," + // <-- added for issue 26 + "\"@ref\":\"1\"" + + "}" + + "}" + + "}," + + + "{\"" + + "@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Inner\"," + // <-- added for issue 26 + "\"@ref\":\"2\"}" + + "]]"; + + // @formatter:on + @Test + public void testDeserializeWithDefaultTyping() throws JsonProcessingException { + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); + ObjectMapper mapper = new ObjectMapper() + .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + JsonTypeInfo.As.PROPERTY); + // also test this when jackson is upgraded... + // List target = mapper.readerForListOf(Object.class).readValue(json); + mapper.readValue(TEST_JSON, ArrayList.class); + + } + + // classes used in this test + + @SuppressWarnings("unused") + @JsonIdentityInfo(generator = JSOGGenerator.class) + public static class Outer { + private Inner inner; + + public Inner getInner() { + return inner; + } + + public void setInner(Inner inner) { + this.inner = inner; + } + } + + @SuppressWarnings("unused") + @JsonIdentityInfo(generator = JSOGGenerator.class) + public static class Inner { + + private Outer outer; + + Inner() { + } + + public Inner(Outer outer) { + this.outer = outer; + outer.inner = this; + } + + public Outer getOuter() { + return outer; + } + + public void setOuter(Outer outer) { + this.outer = outer; + } + } + + +} From 4f0cd2848a14a37658dc7ffdd098fcad76b4b567 Mon Sep 17 00:00:00 2001 From: gus Date: Thu, 26 Jun 2025 21:53:18 -0400 Subject: [PATCH 02/11] fix indent --- .../com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java index fc32113..04ddc39 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java @@ -40,9 +40,9 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ if (p.currentToken() != JsonToken.VALUE_STRING) { throw new IllegalStateException("@ref attribute should be followed by a value?"); } - String text = p.getText(); //grab our id - p.nextToken(); // consume END_OBJECT - return new JSOGRef(text); + String text = p.getText(); //grab our id + p.nextToken(); // consume END_OBJECT + return new JSOGRef(text); } else { throw new IllegalStateException("Unexpected Token Type:" + p.currentToken().name()); } From 1250fc41ad8d1ffd19dab54022f0f1e6d2e0d580 Mon Sep 17 00:00:00 2001 From: gus Date: Thu, 26 Jun 2025 21:55:33 -0400 Subject: [PATCH 03/11] touch up javadoc warning --- .../java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java index 04ddc39..6d8b770 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; /** - * Knows how to take either form of a JSOGRef (string or {@ref:string} and convert it back into a JSOGRef. + * Knows how to take either form of a JSOGRef (string or {@code {@ref:string}}) and convert it back into a JSOGRef. * * @author Jeff Schnitzer */ From 4afdd3af6ddbea54c373f9da83113ffeeb162d5f Mon Sep 17 00:00:00 2001 From: gus Date: Fri, 27 Jun 2025 11:54:47 -0400 Subject: [PATCH 04/11] failing serialization test for default typing --- .../voodoodyne/jackson/jsog/Issue26Test.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index c7f3cb7..d8476c6 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -1,6 +1,9 @@ package com.voodoodyne.jackson.jsog; +import static org.junit.Assert.assertEquals; + import java.util.ArrayList; +import java.util.List; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonProcessingException; @@ -8,6 +11,7 @@ import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import org.junit.Test; +import org.testng.collections.Lists; public class Issue26Test { @@ -45,6 +49,23 @@ public void testDeserializeWithDefaultTyping() throws JsonProcessingException { } + @Test + public void testSerializeWithDefaultTypeing() throws JsonProcessingException { + Outer outer = new Outer(); + Inner inner = new Inner(outer); + // Turn on type info + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); + ObjectMapper mapper = new ObjectMapper() + .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + JsonTypeInfo.As.PROPERTY); + + + List source = Lists.newArrayList(outer, inner); + String json = mapper.writeValueAsString(source); + + assertEquals(TEST_JSON,json); + } + // classes used in this test @SuppressWarnings("unused") From 9c99ef31191442da9fb6d185374585714874fb30 Mon Sep 17 00:00:00 2001 From: gus Date: Fri, 27 Jun 2025 13:02:23 -0400 Subject: [PATCH 05/11] We can get type information added correctly, but we don't have a way to not add it in the event that default typing is NOT on. Jackson doesn't seem to provide any means for serializers to know if default typing is turned on or not. This is amounting to a lot of code that tries to not touch the original (nifty) deserialize method using JsonNode, but it might be simpler to change that since it is fundamentally treating a reference as a full node and Jackson isn't expecting that AFAICT. --- .../jackson/jsog/JSOGGenerator.java | 2 +- .../com/voodoodyne/jackson/jsog/JSOGRef.java | 4 ++- .../jackson/jsog/JSOGRefSerializer.java | 4 +-- .../voodoodyne/jackson/jsog/Issue26Test.java | 25 +++++++++++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java index da527bb..3b6c796 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java @@ -52,7 +52,7 @@ public com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey key(Object key) public JSOGRef generateId(Object forPojo) { int id = _nextValue; ++_nextValue; - return new JSOGRef(id); + return new JSOGRef(id, forPojo); } @Override diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRef.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRef.java index abff9c2..2b13b4f 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRef.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRef.java @@ -22,6 +22,7 @@ public final class JSOGRef @JsonProperty(REF_KEY) public String ref; + transient Object refTo; /** * A flag we use to determine if this ref has already been serialized. Because jackson calls the same * code for serializing both ids and refs, we simply assume the first use is an id and all subsequent @@ -35,8 +36,9 @@ public JSOGRef(String val) { } /** */ - public JSOGRef(int val) { + public JSOGRef(int val, Object refTo) { this(Integer.toString(val)); + this.refTo = refTo; } @Override diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java index 0f7f23e..e5d33e4 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java @@ -4,7 +4,6 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; @@ -16,9 +15,10 @@ public class JSOGRefSerializer extends JsonSerializer { @Override - public void serialize(JSOGRef value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + public void serialize(JSOGRef value, JsonGenerator jgen, SerializerProvider provider) throws IOException { if (value.used) { jgen.writeStartObject(); + jgen.writeObjectField("@class", value.refTo.getClass().getName()); jgen.writeObjectField(JSOGRef.REF_KEY, value.ref); jgen.writeEndObject(); } else { diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index d8476c6..04ae5e5 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -15,8 +15,8 @@ public class Issue26Test { - // @formatter:off - String TEST_JSON="[\"java.util.ArrayList\",[" + + public static final String WIHOUT_DEFAULT_TYPING = "[{\"@id\":\"1\",\"inner\":{\"@id\":\"2\",\"outer\":{\"@ref\":\"1\"}}},{\"@ref\":\"2\"}]"; + public static final String TEST_JSON="[\"java.util.ArrayList\",[" + "{" + "\"@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Outer\"," + @@ -66,6 +66,27 @@ public void testSerializeWithDefaultTypeing() throws JsonProcessingException { assertEquals(TEST_JSON,json); } + // This will also be caught by various other tests, but for completeness of this issue + // we should also explicitly test it here with the same objects + @Test + public void testSerializeWithoutDefaultTypeing() throws JsonProcessingException { + Outer outer = new Outer(); + Inner inner = new Inner(outer); + // Turn on type info + ObjectMapper mapper = new ObjectMapper(); + + List source = Lists.newArrayList(outer, inner); + String json = mapper.writeValueAsString(source); + + // make sure our test json is truly valid - this round trips through a list of maps + @SuppressWarnings("rawtypes") + ArrayList l = mapper.readValue(WIHOUT_DEFAULT_TYPING, ArrayList.class); + assertEquals(WIHOUT_DEFAULT_TYPING, mapper.writeValueAsString(l)); + + // type info should not be written unless Default typing is on + assertEquals(WIHOUT_DEFAULT_TYPING,json); + } + // classes used in this test @SuppressWarnings("unused") From 0b96e70111ff81f4b084a863d125fb9c7f6f020d Mon Sep 17 00:00:00 2001 From: gus Date: Sat, 28 Jun 2025 17:25:01 -0400 Subject: [PATCH 06/11] I can't find any good way to directly detect if default typing is on, or what the applicable attribute name might be, but it looks like we can at least set this as a serialization provider attribute. Ideally we would just query the jackson internals, but at least this can be configured at the time we create the mapper --- .../java/com/voodoodyne/jackson/jsog/JSOGGenerator.java | 3 ++- .../com/voodoodyne/jackson/jsog/JSOGRefSerializer.java | 5 ++++- .../java/com/voodoodyne/jackson/jsog/Issue26Test.java | 9 ++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java index 3b6c796..df60be3 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java @@ -11,7 +11,8 @@ */ public class JSOGGenerator extends ObjectIdGenerator { - private static final long serialVersionUID = 1L; + public static final Object DEFAULT_TYPING = "JSOG_DEFAULT_TYPING_ATTRIBUTE"; + private static final long serialVersionUID = 1L; protected transient int _nextValue; protected final Class _scope; diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java index e5d33e4..4dc2494 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java @@ -18,7 +18,10 @@ public class JSOGRefSerializer extends JsonSerializer public void serialize(JSOGRef value, JsonGenerator jgen, SerializerProvider provider) throws IOException { if (value.used) { jgen.writeStartObject(); - jgen.writeObjectField("@class", value.refTo.getClass().getName()); + Object attribute = provider.getAttribute(JSOGGenerator.DEFAULT_TYPING); + if (attribute != null) { + jgen.writeObjectField(attribute.toString(), value.refTo.getClass().getName()); + } jgen.writeObjectField(JSOGRef.REF_KEY, value.ref); jgen.writeEndObject(); } else { diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index 04ae5e5..f6756b0 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import org.junit.Test; @@ -58,8 +59,14 @@ public void testSerializeWithDefaultTypeing() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper() .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY); + // sadly the following throws a null pointer exception + // mapper.getSerializerProvider().setAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); - + // probably there is a better way to do this but it wasn't easy to find quickly. + SerializationConfig config = mapper.getSerializationConfig().withAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); + mapper = new ObjectMapper() + .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + JsonTypeInfo.As.PROPERTY).setConfig(config); List source = Lists.newArrayList(outer, inner); String json = mapper.writeValueAsString(source); From bf42a1409fd28422b5abe238993be560143d7c0c Mon Sep 17 00:00:00 2001 From: gus Date: Sat, 28 Jun 2025 17:40:52 -0400 Subject: [PATCH 07/11] touch-ups, better assertions on the key test. --- .../com/voodoodyne/jackson/jsog/JSOGGenerator.java | 2 +- .../voodoodyne/jackson/jsog/JSOGRefSerializer.java | 2 +- .../com/voodoodyne/jackson/jsog/Issue26Test.java | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java index df60be3..9b0afd0 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java @@ -11,7 +11,7 @@ */ public class JSOGGenerator extends ObjectIdGenerator { - public static final Object DEFAULT_TYPING = "JSOG_DEFAULT_TYPING_ATTRIBUTE"; + public static final Object DEFAULT_TYPING_ATTRIBUTE = "JSOG_DEFAULT_TYPING_ATTRIBUTE"; private static final long serialVersionUID = 1L; protected transient int _nextValue; diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java index 4dc2494..8244e17 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java @@ -18,7 +18,7 @@ public class JSOGRefSerializer extends JsonSerializer public void serialize(JSOGRef value, JsonGenerator jgen, SerializerProvider provider) throws IOException { if (value.used) { jgen.writeStartObject(); - Object attribute = provider.getAttribute(JSOGGenerator.DEFAULT_TYPING); + Object attribute = provider.getAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE); if (attribute != null) { jgen.writeObjectField(attribute.toString(), value.refTo.getClass().getName()); } diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index f6756b0..bd245c4 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -1,6 +1,7 @@ package com.voodoodyne.jackson.jsog; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import java.util.ArrayList; import java.util.List; @@ -46,8 +47,13 @@ public void testDeserializeWithDefaultTyping() throws JsonProcessingException { JsonTypeInfo.As.PROPERTY); // also test this when jackson is upgraded... // List target = mapper.readerForListOf(Object.class).readValue(json); - mapper.readValue(TEST_JSON, ArrayList.class); - + @SuppressWarnings("rawtypes") + ArrayList list = mapper.readValue(TEST_JSON, ArrayList.class); + assertEquals(2, list.size()); + assertEquals(Outer.class, list.get(0).getClass()); // prove it's not a list of map objects + assertEquals(Inner.class, list.get(1).getClass()); + assertSame(((Outer)list.get(0)).inner, list.get(1)); + assertSame(((Inner)list.get(1)).outer, list.get(0)); } @Test @@ -63,13 +69,12 @@ public void testSerializeWithDefaultTypeing() throws JsonProcessingException { // mapper.getSerializerProvider().setAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); // probably there is a better way to do this but it wasn't easy to find quickly. - SerializationConfig config = mapper.getSerializationConfig().withAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); + SerializationConfig config = mapper.getSerializationConfig().withAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE, "@class"); mapper = new ObjectMapper() .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY).setConfig(config); List source = Lists.newArrayList(outer, inner); String json = mapper.writeValueAsString(source); - assertEquals(TEST_JSON,json); } @@ -133,5 +138,4 @@ public void setOuter(Outer outer) { } } - } From 46dddaac8c8c7af7508eb0ee4336de910d4edbdf Mon Sep 17 00:00:00 2001 From: gus Date: Sat, 28 Jun 2025 19:03:52 -0400 Subject: [PATCH 08/11] Demonstrate what forms of default typing work in a unit test. --- .../jackson/jsog/JSOGGenerator.java | 2 +- .../jackson/jsog/JSOGRefSerializer.java | 1 - .../voodoodyne/jackson/jsog/Issue26Test.java | 92 +++++++++++++++++-- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java index 9b0afd0..95f2865 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGGenerator.java @@ -11,7 +11,7 @@ */ public class JSOGGenerator extends ObjectIdGenerator { - public static final Object DEFAULT_TYPING_ATTRIBUTE = "JSOG_DEFAULT_TYPING_ATTRIBUTE"; + public static final String DEFAULT_TYPING_ATTRIBUTE = "JSOG_DEFAULT_TYPING_ATTRIBUTE"; private static final long serialVersionUID = 1L; protected transient int _nextValue; diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java index 8244e17..4a06623 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java @@ -2,7 +2,6 @@ import java.io.IOException; - import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index bd245c4..cee36c4 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -4,22 +4,29 @@ import static org.junit.Assert.assertSame; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.testng.collections.Lists; +@RunWith(Parameterized.class) public class Issue26Test { public static final String WIHOUT_DEFAULT_TYPING = "[{\"@id\":\"1\",\"inner\":{\"@id\":\"2\",\"outer\":{\"@ref\":\"1\"}}},{\"@ref\":\"2\"}]"; - public static final String TEST_JSON="[\"java.util.ArrayList\",[" + + // @formatter:off + public static final String TEST_JSON= + "[\"java.util.ArrayList\",[" + "{" + "\"@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Outer\"," + "\"@id\":\"1\"," + @@ -37,14 +44,40 @@ public class Issue26Test { "@class\":\"com.voodoodyne.jackson.jsog.Issue26Test$Inner\"," + // <-- added for issue 26 "\"@ref\":\"2\"}" + "]]"; - // @formatter:on + + private DefaultTyping defaultTyping; + + public Issue26Test(DefaultTyping defaultTyping) { + this.defaultTyping = defaultTyping; + } + + @Parameterized.Parameters + public static Collection data() { + ArrayList objects = new ArrayList(); + // sadly these don't work I tried adding a second attribute to let us know which + // mode we are in, but adding @class in these cases throws an exception because + // @class is not expected in one place and not adding it throws exceptions + // when @class isn't found in another. There's some tricky subtlety in these formats + // relating to typing I haven't figured out + // objects.add(new Object[]{DefaultTyping.JAVA_LANG_OBJECT}); + // objects.add(new Object[]{DefaultTyping.OBJECT_AND_NON_CONCRETE}); + // objects.add(new Object[]{DefaultTyping.NON_CONCRETE_AND_ARRAYS}); + + // These work + objects.add(new Object[]{DefaultTyping.NON_FINAL}); + objects.add(new Object[]{DefaultTyping.EVERYTHING}); + return objects; + } + + @Test public void testDeserializeWithDefaultTyping() throws JsonProcessingException { PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); ObjectMapper mapper = new ObjectMapper() - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY); + System.out.println(defaultTyping); // also test this when jackson is upgraded... // List target = mapper.readerForListOf(Object.class).readValue(json); @SuppressWarnings("rawtypes") @@ -52,18 +85,57 @@ public void testDeserializeWithDefaultTyping() throws JsonProcessingException { assertEquals(2, list.size()); assertEquals(Outer.class, list.get(0).getClass()); // prove it's not a list of map objects assertEquals(Inner.class, list.get(1).getClass()); - assertSame(((Outer)list.get(0)).inner, list.get(1)); - assertSame(((Inner)list.get(1)).outer, list.get(0)); + assertSame(((Outer) list.get(0)).inner, list.get(1)); + assertSame(((Inner) list.get(1)).outer, list.get(0)); + } + + @Test + public void testRoundTrip() throws JsonProcessingException { + + Outer outer = new Outer(); + Inner inner = new Inner(outer); + // Turn on type info + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); + ObjectMapper mapper = new ObjectMapper() + .activateDefaultTyping(ptv, defaultTyping, + JsonTypeInfo.As.PROPERTY); + // sadly the following throws a null pointer exception + // mapper.getSerializerProvider().setAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); + + // probably there is a better way to do this without double instantiating the mapper, + // but it wasn't easy to find. One can also set this directly when invoking the write + // operations with mapper.writer().withAttribute(JSOGGenerator.DEFAULT_TYPING, "@class") + // if single instantiation is important, but then you need to do it every time you write. + SerializationConfig config = mapper.getSerializationConfig() + // obviously this attribute must be coordinated with the actual value used in the JSON + // (could also be "@c" or a custom name depending on config) + .withAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE, "@class"); + + mapper = new ObjectMapper() + .activateDefaultTyping(ptv, defaultTyping, + JsonTypeInfo.As.PROPERTY).setConfig(config); + List source = Lists.newArrayList(outer, inner); + String json = mapper.writeValueAsString(source); + + // also test this when jackson is upgraded... + // List target = mapper.readerForListOf(Object.class).readValue(json); + @SuppressWarnings("rawtypes") + ArrayList list = mapper.readValue(json, ArrayList.class); + assertEquals(2, list.size()); + assertEquals(Outer.class, list.get(0).getClass()); // prove it's not a list of map objects + assertEquals(Inner.class, list.get(1).getClass()); + assertSame(((Outer) list.get(0)).inner, list.get(1)); + assertSame(((Inner) list.get(1)).outer, list.get(0)); } @Test - public void testSerializeWithDefaultTypeing() throws JsonProcessingException { + public void testSerializeWithDefaultTyping() throws JsonProcessingException { Outer outer = new Outer(); Inner inner = new Inner(outer); // Turn on type info PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); ObjectMapper mapper = new ObjectMapper() - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY); // sadly the following throws a null pointer exception // mapper.getSerializerProvider().setAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); @@ -71,11 +143,11 @@ public void testSerializeWithDefaultTypeing() throws JsonProcessingException { // probably there is a better way to do this but it wasn't easy to find quickly. SerializationConfig config = mapper.getSerializationConfig().withAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE, "@class"); mapper = new ObjectMapper() - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY).setConfig(config); List source = Lists.newArrayList(outer, inner); String json = mapper.writeValueAsString(source); - assertEquals(TEST_JSON,json); + assertEquals(TEST_JSON, json); } // This will also be caught by various other tests, but for completeness of this issue @@ -96,7 +168,7 @@ public void testSerializeWithoutDefaultTypeing() throws JsonProcessingException assertEquals(WIHOUT_DEFAULT_TYPING, mapper.writeValueAsString(l)); // type info should not be written unless Default typing is on - assertEquals(WIHOUT_DEFAULT_TYPING,json); + assertEquals(WIHOUT_DEFAULT_TYPING, json); } // classes used in this test From cfb4d372d39ffb99c605851f5c2db772867243fa Mon Sep 17 00:00:00 2001 From: gus Date: Sat, 28 Jun 2025 19:52:14 -0400 Subject: [PATCH 09/11] Documentation, and notes on a trappy combination that almost works but not quite. --- README.md | 12 ++++++++ .../voodoodyne/jackson/jsog/Issue26Test.java | 29 ++++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3416657..95e86fe 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ To use this plugin, annotate any classes which may contain references with *@Jso String name; Person secretSanta; } + +### Usage with Default Typing + +If you are using [default typing](https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization) to handle polymorphism there is a little more setup you will need. +In short, you need to supply an attribute to the serializer. +This attribute will signal that default typing is in play and specify the attribute name to use for expressing types (Usually this is `@class`). +Examples and important warnings can be seen in [the unit test](blob/master/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java) for DefaultTyping. Currently only NON_FINAL and EVERYTHING modes are supported. + +**WARNING:** Default typing modes other than NON_FINAL and EVERYTHING may appear to succeed when `.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)` is used **but actually cause buggy deserialization** in which objects may become duplicated. +This is why only these two modes are currently supported (see note at end of `testRoundTrip()` in the unit test for details, enhancements welcome) + + ## Author diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index cee36c4..8311c83 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -1,5 +1,7 @@ package com.voodoodyne.jackson.jsog; +import static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.EVERYTHING; +import static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -22,7 +24,7 @@ @RunWith(Parameterized.class) public class Issue26Test { - public static final String WIHOUT_DEFAULT_TYPING = "[{\"@id\":\"1\",\"inner\":{\"@id\":\"2\",\"outer\":{\"@ref\":\"1\"}}},{\"@ref\":\"2\"}]"; + public static final String WITHOUT_DEFAULT_TYPING = "[{\"@id\":\"1\",\"inner\":{\"@id\":\"2\",\"outer\":{\"@ref\":\"1\"}}},{\"@ref\":\"2\"}]"; // @formatter:off public static final String TEST_JSON= @@ -46,7 +48,7 @@ public class Issue26Test { "]]"; // @formatter:on - private DefaultTyping defaultTyping; + private final DefaultTyping defaultTyping; public Issue26Test(DefaultTyping defaultTyping) { this.defaultTyping = defaultTyping; @@ -65,8 +67,8 @@ public static Collection data() { // objects.add(new Object[]{DefaultTyping.NON_CONCRETE_AND_ARRAYS}); // These work - objects.add(new Object[]{DefaultTyping.NON_FINAL}); - objects.add(new Object[]{DefaultTyping.EVERYTHING}); + objects.add(new Object[]{NON_FINAL}); + objects.add(new Object[]{EVERYTHING}); return objects; } @@ -75,7 +77,7 @@ public static Collection data() { public void testDeserializeWithDefaultTyping() throws JsonProcessingException { PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); ObjectMapper mapper = new ObjectMapper() - .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, EVERYTHING, JsonTypeInfo.As.PROPERTY); System.out.println(defaultTyping); // also test this when jackson is upgraded... @@ -112,6 +114,10 @@ public void testRoundTrip() throws JsonProcessingException { .withAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE, "@class"); mapper = new ObjectMapper() + // FAIL_ON_UNKNOWN_PROPERTIES = false is dangerous if combined with any of + // JAVA_LANG_OBJECT, OBJECT_AND_NON_CONCRETE, NON_CONCRETE_AND_ARRAYS + // The final line of this test demonstrates that it can lead to duplication of objects. + // .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .activateDefaultTyping(ptv, defaultTyping, JsonTypeInfo.As.PROPERTY).setConfig(config); List source = Lists.newArrayList(outer, inner); @@ -125,6 +131,9 @@ public void testRoundTrip() throws JsonProcessingException { assertEquals(Outer.class, list.get(0).getClass()); // prove it's not a list of map objects assertEquals(Inner.class, list.get(1).getClass()); assertSame(((Outer) list.get(0)).inner, list.get(1)); + + // This is critical. It fails when .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // and any of JAVA_LANG_OBJECT, OBJECT_AND_NON_CONCRETE, NON_CONCRETE_AND_ARRAYS are used; assertSame(((Inner) list.get(1)).outer, list.get(0)); } @@ -135,7 +144,7 @@ public void testSerializeWithDefaultTyping() throws JsonProcessingException { // Turn on type info PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(); ObjectMapper mapper = new ObjectMapper() - .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, EVERYTHING, JsonTypeInfo.As.PROPERTY); // sadly the following throws a null pointer exception // mapper.getSerializerProvider().setAttribute(JSOGGenerator.DEFAULT_TYPING, "@class"); @@ -143,7 +152,7 @@ public void testSerializeWithDefaultTyping() throws JsonProcessingException { // probably there is a better way to do this but it wasn't easy to find quickly. SerializationConfig config = mapper.getSerializationConfig().withAttribute(JSOGGenerator.DEFAULT_TYPING_ATTRIBUTE, "@class"); mapper = new ObjectMapper() - .activateDefaultTyping(ptv, DefaultTyping.EVERYTHING, + .activateDefaultTyping(ptv, EVERYTHING, JsonTypeInfo.As.PROPERTY).setConfig(config); List source = Lists.newArrayList(outer, inner); String json = mapper.writeValueAsString(source); @@ -164,11 +173,11 @@ public void testSerializeWithoutDefaultTypeing() throws JsonProcessingException // make sure our test json is truly valid - this round trips through a list of maps @SuppressWarnings("rawtypes") - ArrayList l = mapper.readValue(WIHOUT_DEFAULT_TYPING, ArrayList.class); - assertEquals(WIHOUT_DEFAULT_TYPING, mapper.writeValueAsString(l)); + ArrayList l = mapper.readValue(WITHOUT_DEFAULT_TYPING, ArrayList.class); + assertEquals(WITHOUT_DEFAULT_TYPING, mapper.writeValueAsString(l)); // type info should not be written unless Default typing is on - assertEquals(WIHOUT_DEFAULT_TYPING, json); + assertEquals(WITHOUT_DEFAULT_TYPING, json); } // classes used in this test From 4fe50df808819f707ef6e9b446ba401028403d21 Mon Sep 17 00:00:00 2001 From: gus Date: Sun, 29 Jun 2025 16:21:50 -0400 Subject: [PATCH 10/11] Add a couple comments --- .../com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java | 5 +++++ .../com/voodoodyne/jackson/jsog/JSOGRefSerializer.java | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java index 6d8b770..4238413 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefDeserializer.java @@ -36,6 +36,11 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ if (p.currentToken() == JsonToken.FIELD_NAME) { // we are being called for { "@ref":"#" } // and starting here ------^ + // Or in default typing we should get called for { "@class":"com.example.Thingy","@ref":"#" } + // and be starting here --------------------------------------------------------^ + // because default typing will have consumed the type attribute already. See the javadoc for + // com.fasterxml.jackson.databind.JsonDeserializer.deserialize(JsonParser, DeserializationContext) + // which talks about this (yes that does seem slightly out of place, I'd guess it predates this method) p.nextToken(); if (p.currentToken() != JsonToken.VALUE_STRING) { throw new IllegalStateException("@ref attribute should be followed by a value?"); diff --git a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java index 4a06623..b6f8fdc 100644 --- a/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java +++ b/src/main/java/com/voodoodyne/jackson/jsog/JSOGRefSerializer.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; /** * Knows how to take a JSOGRef and print it as @id or @ref as appropriate. @@ -29,4 +30,11 @@ public void serialize(JSOGRef value, JsonGenerator jgen, SerializerProvider prov } } +// Side Note: This never gets called, so we cant rely on it despite the fact we need its equivalent in the +// deserializer. I suspect ID serialization is not a first class citizen and jackson isn't expecting typing. +// +// @Override +// public void serializeWithType(JSOGRef value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException { +// super.serializeWithType(value, gen, serializers, typeSer); +// } } From 9e10807f595b4fbee91c7466f440d28c33b8c392 Mon Sep 17 00:00:00 2001 From: gus Date: Tue, 5 Aug 2025 00:29:26 -0400 Subject: [PATCH 11/11] Comment about an improvement available in newer Jackson --- src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java index 8311c83..d901eba 100644 --- a/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java +++ b/src/test/java/com/voodoodyne/jackson/jsog/Issue26Test.java @@ -108,6 +108,9 @@ public void testRoundTrip() throws JsonProcessingException { // but it wasn't easy to find. One can also set this directly when invoking the write // operations with mapper.writer().withAttribute(JSOGGenerator.DEFAULT_TYPING, "@class") // if single instantiation is important, but then you need to do it every time you write. + // + // Note: there IS a better way after https://github.com/FasterXML/jackson-databind/issues/3001 + // but that requires 2.13.0+ jackson dependency. SerializationConfig config = mapper.getSerializationConfig() // obviously this attribute must be coordinated with the actual value used in the JSON // (could also be "@c" or a custom name depending on config)