diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonParser.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonParser.java
index 706068237..1948b9fe3 100644
--- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonParser.java
+++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonParser.java
@@ -53,6 +53,18 @@ public enum Feature implements FormatFeature // in 2.12
* @since 2.12.3
*/
USE_NATIVE_TYPE_ID(true),
+ /**
+ * Whether to convert "null" to an IonValueNull (true);
+ * or leave as a java null (false) when deserializing.
+ *
+ * Enabled by default for backwards compatibility as that has been the behavior
+ * of `jackson-dataformat-ion` since 2.13.
+ *
+ * @see The Ion Specification
+ *
+ * @since 2.19.0
+ */
+ READ_NULL_AS_IONVALUE(true),
;
final boolean _defaultState;
@@ -563,6 +575,17 @@ private IonValue getIonValue() throws IOException {
writer.writeValue(_reader);
IonValue v = l.get(0);
v.removeFromContainer();
+
+ if (!Feature.READ_NULL_AS_IONVALUE.enabledIn(_formatFeatures)) {
+ // 2025-04-11, seadbrane: The default is to read 'null' as an Ion Null object.
+ // However, there is no way to determine from the serialized ion data if a 'null'
+ // was an IonNullValue or a 'null' container type such as IonNullStruct or IonNullList.
+ // So if READ_NULL_AS_IONVALUE is disabled, then return 'null' if the _valueToken
+ // is 'null' and the Ion value read is not container type already.
+ if (v.isNullValue() && _valueToken == JsonToken.VALUE_NULL && !IonType.isContainer(v.getType())) {
+ return null;
+ }
+ }
return v;
}
diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializer.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializer.java
index 0835a989b..bd04adf7c 100644
--- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializer.java
+++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializer.java
@@ -16,21 +16,40 @@
import java.io.IOException;
-import com.amazon.ion.*;
-
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.util.AccessPattern;
import com.fasterxml.jackson.dataformat.ion.IonParser;
+import com.amazon.ion.*;
/**
* Deserializer that knows how to deserialize an IonValue.
*/
-class IonValueDeserializer extends JsonDeserializer {
+class IonValueDeserializer extends JsonDeserializer implements ContextualDeserializer {
+
+ private final JavaType _targetType;
+
+ public IonValueDeserializer() {
+ this._targetType = null;
+ }
+
+ public IonValueDeserializer(JavaType targetType) {
+ this._targetType = targetType;
+ }
+
+ @Override
+ public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) {
+ JavaType contextualType = (property != null)
+ ? property.getType()
+ : ctxt.getContextualType(); // fallback
+ return new IonValueDeserializer(contextualType);
+ }
@Override
public IonValue deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+
Object embeddedObject = jp.getEmbeddedObject();
if (embeddedObject instanceof IonValue) {
return (IonValue) embeddedObject;
@@ -62,17 +81,34 @@ public IonValue getNullValue(DeserializationContext ctxt) throws JsonMappingExce
if (embeddedObj instanceof IonValue) {
IonValue iv = (IonValue) embeddedObj;
if (iv.isNullValue()) {
+ if (IonType.isContainer(iv.getType())) {
+ return iv;
+ }
+ IonType containerType = getIonContainerType();
+ if (containerType != null) {
+ IonSystem ionSystem = ((IonParser) parser).getIonSystem();
+ return ionSystem.newNull(containerType);
+ }
return iv;
}
}
}
-
return super.getNullValue(ctxt);
} catch (IOException e) {
throw JsonMappingException.from(ctxt, e.toString());
}
}
+ private IonType getIonContainerType() {
+ if (_targetType != null) {
+ Class> clazz = _targetType.getRawClass();
+ if (IonStruct.class.isAssignableFrom(clazz)) return IonType.STRUCT;
+ if (IonList.class.isAssignableFrom(clazz)) return IonType.LIST;
+ if (IonSexp.class.isAssignableFrom(clazz)) return IonType.SEXP;
+ }
+ return null;
+ }
+
@Override
public AccessPattern getNullAccessPattern() {
return AccessPattern.DYNAMIC;
diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializerTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializerTest.java
index 7c97012e6..430cba2fa 100644
--- a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializerTest.java
+++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializerTest.java
@@ -1,17 +1,25 @@
package com.fasterxml.jackson.dataformat.ion.ionvalue;
-import java.io.IOException;
-import java.util.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import com.amazon.ion.IonSystem;
+import com.amazon.ion.IonValue;
+import com.amazon.ion.IonStruct;
-import com.amazon.ion.*;
-import com.amazon.ion.system.IonSystemBuilder;
import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.annotation.*;
+import com.amazon.ion.system.IonSystemBuilder;
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.util.AccessPattern;
import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
+import com.fasterxml.jackson.dataformat.ion.IonParser;
-import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -65,7 +73,10 @@ static class IonValueData extends Data {
}
private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
- private static final IonValueMapper ION_VALUE_MAPPER = new IonValueMapper(SYSTEM, SNAKE_CASE);
+ private static final IonValueMapper ION_VALUE_MAPPER
+ = new IonValueMapper(SYSTEM, PropertyNamingStrategies.SNAKE_CASE);
+ private static final IonValueMapper ION_MAPPER_READ_NULL_DISABLED
+ = (IonValueMapper) new IonValueMapper(SYSTEM, PropertyNamingStrategies.SNAKE_CASE).disable(IonParser.Feature.READ_NULL_AS_IONVALUE);
@Test
public void shouldBeAbleToDeserialize() throws Exception {
@@ -91,24 +102,48 @@ public void shouldBeAbleToDeserializeIncludingNullList() throws Exception {
assertEquals(ion("null.list"), data.getAllData().get("c"));
}
+ @Test
+ public void shouldBeAbleToDeserializeNullToIonNull() throws Exception {
+ verifyNullDeserialization("{c:null}", SYSTEM.newNull(), null);
+ }
+
@Test
public void shouldBeAbleToDeserializeNullList() throws Exception {
- IonValue ion = ion("{c:null.list}");
+ verifyNullDeserialization("{c:null.list}", SYSTEM.newNullList());
+ }
- IonValueData data = ION_VALUE_MAPPER.readValue(ion, IonValueData.class);
- assertEquals(1, data.getAllData().size());
- assertEquals(SYSTEM.newNullList(), data.getAllData().get("c"));
- }
@Test
public void shouldBeAbleToDeserializeNullStruct() throws Exception {
- IonValue ion = ion("{c:null.struct}");
+ verifyNullDeserialization("{c:null.struct}", SYSTEM.newNullStruct());
+ }
- IonValueData data = ION_VALUE_MAPPER.readValue(ion, IonValueData.class);
+ @Test
+ public void shouldBeAbleToDeserializeNullSexp() throws Exception {
+ verifyNullDeserialization("{c:null.sexp}", SYSTEM.newNullSexp());
+ }
+
+ private void verifyNullDeserialization(String ionString, IonValue expected) throws Exception {
+ verifyNullDeserialization(ionString, expected, expected);
+ }
+
+ private void verifyNullDeserialization(String ionString, IonValue expected, IonValue expectedReadNullDisabled) throws Exception {
+ verifyNullDeserialization(ION_VALUE_MAPPER, ionString, expected);
+ verifyNullDeserialization(ION_MAPPER_READ_NULL_DISABLED, ionString, expectedReadNullDisabled);
+ }
+
+ private void verifyNullDeserialization(IonValueMapper mapper, String ionString, IonValue expected) throws Exception {
+ IonValueData data = mapper.readValue(ionString, IonValueData.class);
+
+ assertEquals(1, data.getAllData().size());
+ assertEquals(expected, data.getAllData().get("c"));
+
+ IonValue ion = ion(ionString);
+ data = mapper.readValue(ion, IonValueData.class);
assertEquals(1, data.getAllData().size());
- assertEquals(SYSTEM.newNullStruct(), data.getAllData().get("c"));
+ assertEquals(expected, data.getAllData().get("c"));
}
@Test
@@ -154,6 +189,22 @@ public void shouldBeAbleToSerializeAndDeserializePojo() throws Exception {
assertEquals(source, result);
}
+ @Test
+ public void shouldBeAbleToSerializeAndDeserializeIonValueDataWithIncludeNonNull() throws Exception {
+ IonValueData source = new IonValueData();
+ source.put("a", SYSTEM.newInt(1));
+ source.put("b", SYSTEM.newNull());
+ source.put("c", null);
+ IonValueMapper mapper = (IonValueMapper) ION_VALUE_MAPPER.copy().setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+ String data = mapper.writeValueAsString(source);
+ assertEquals("{a:1,b:null}", data);
+ // Now remove the null element for the comparison below.
+ source.getAllData().remove("c");
+ IonValueData result = mapper.readValue(data, IonValueData.class);
+ assertEquals(source, result);
+ }
+
@Test
public void shouldBeAbleToSerializeAndDeserializeStringData() throws Exception {
StringData source = new StringData();
@@ -162,7 +213,17 @@ public void shouldBeAbleToSerializeAndDeserializeStringData() throws Exception {
IonValue data = ION_VALUE_MAPPER.writeValueAsIonValue(source);
StringData result = ION_VALUE_MAPPER.parse(data, StringData.class);
+ assertEquals(source, result);
+ }
+
+ @Test
+ public void shouldBeAbleToSerializeAndDeserializeStringDataAsString() throws Exception {
+ StringData source = new StringData();
+ source.put("a", "1");
+ source.put("b", null);
+ String data = ION_VALUE_MAPPER.writeValueAsString(source);
+ StringData result = ION_VALUE_MAPPER.readValue(data, StringData.class);
assertEquals(source, result);
}
@@ -180,7 +241,7 @@ static class MyBean {
}
@Test
- public void testWithMissingProperty() throws IOException
+ public void testWithMissingProperty() throws Exception
{
IonSystem ionSystem = IonSystemBuilder.standard().build();
IonObjectMapper ionObjectMapper = IonObjectMapper.builder(ionSystem)
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index 6348ee0a8..8dec9da09 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -381,3 +381,7 @@ Cormac Redmond (@credmond)
Manuel Sugawara (@sugmanue)
* Contributed #568: Improve ASCII decoding performance for `CBORParser`
(2.19.0)
+
+Josh Curry (@seadbrane)
+ * Reported, contributed fix for #571: Unable to deserialize a pojo with IonStruct
+ (2.19.0)
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 135d01caa..213be7c18 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -14,6 +14,9 @@ Active maintainers:
=== Releases ===
------------------------------------------------------------------------
+#571: Unable to deserialize a pojo with IonStruct
+ (reported, fix contributed by Josh C)
+
2.19.0-rc2 (07-Apr-2025)
#300: (smile) Floats are encoded with sign extension while doubles without