Skip to content

Commit ad27eaa

Browse files
authored
Fix #571: Unable to deserialize a pojo with IonStruct (#573)
1 parent ac24dad commit ad27eaa

File tree

5 files changed

+147
-20
lines changed

5 files changed

+147
-20
lines changed

Diff for: ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonParser.java

+23
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ public enum Feature implements FormatFeature // in 2.12
5353
* @since 2.12.3
5454
*/
5555
USE_NATIVE_TYPE_ID(true),
56+
/**
57+
* Whether to convert "null" to an IonValueNull (true);
58+
* or leave as a java null (false) when deserializing.
59+
*<p>
60+
* Enabled by default for backwards compatibility as that has been the behavior
61+
* of `jackson-dataformat-ion` since 2.13.
62+
*
63+
* @see <a href="https://amzn.github.io/ion-docs/docs/spec.html#annot">The Ion Specification</a>
64+
*
65+
* @since 2.19.0
66+
*/
67+
READ_NULL_AS_IONVALUE(true),
5668
;
5769

5870
final boolean _defaultState;
@@ -563,6 +575,17 @@ private IonValue getIonValue() throws IOException {
563575
writer.writeValue(_reader);
564576
IonValue v = l.get(0);
565577
v.removeFromContainer();
578+
579+
if (!Feature.READ_NULL_AS_IONVALUE.enabledIn(_formatFeatures)) {
580+
// 2025-04-11, seadbrane: The default is to read 'null' as an Ion Null object.
581+
// However, there is no way to determine from the serialized ion data if a 'null'
582+
// was an IonNullValue or a 'null' container type such as IonNullStruct or IonNullList.
583+
// So if READ_NULL_AS_IONVALUE is disabled, then return 'null' if the _valueToken
584+
// is 'null' and the Ion value read is not container type already.
585+
if (v.isNullValue() && _valueToken == JsonToken.VALUE_NULL && !IonType.isContainer(v.getType())) {
586+
return null;
587+
}
588+
}
566589
return v;
567590
}
568591

Diff for: ion/src/main/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializer.java

+40-4
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,40 @@
1616

1717
import java.io.IOException;
1818

19-
import com.amazon.ion.*;
20-
2119
import com.fasterxml.jackson.core.JsonParser;
2220
import com.fasterxml.jackson.core.JsonToken;
2321
import com.fasterxml.jackson.databind.*;
22+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
2423
import com.fasterxml.jackson.databind.util.AccessPattern;
2524
import com.fasterxml.jackson.dataformat.ion.IonParser;
25+
import com.amazon.ion.*;
2626

2727
/**
2828
* Deserializer that knows how to deserialize an IonValue.
2929
*/
30-
class IonValueDeserializer extends JsonDeserializer<IonValue> {
30+
class IonValueDeserializer extends JsonDeserializer<IonValue> implements ContextualDeserializer {
31+
32+
private final JavaType _targetType;
33+
34+
public IonValueDeserializer() {
35+
this._targetType = null;
36+
}
37+
38+
public IonValueDeserializer(JavaType targetType) {
39+
this._targetType = targetType;
40+
}
41+
42+
@Override
43+
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
44+
JavaType contextualType = (property != null)
45+
? property.getType()
46+
: ctxt.getContextualType(); // fallback
47+
return new IonValueDeserializer(contextualType);
48+
}
3149

3250
@Override
3351
public IonValue deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
52+
3453
Object embeddedObject = jp.getEmbeddedObject();
3554
if (embeddedObject instanceof IonValue) {
3655
return (IonValue) embeddedObject;
@@ -62,17 +81,34 @@ public IonValue getNullValue(DeserializationContext ctxt) throws JsonMappingExce
6281
if (embeddedObj instanceof IonValue) {
6382
IonValue iv = (IonValue) embeddedObj;
6483
if (iv.isNullValue()) {
84+
if (IonType.isContainer(iv.getType())) {
85+
return iv;
86+
}
87+
IonType containerType = getIonContainerType();
88+
if (containerType != null) {
89+
IonSystem ionSystem = ((IonParser) parser).getIonSystem();
90+
return ionSystem.newNull(containerType);
91+
}
6592
return iv;
6693
}
6794
}
6895
}
69-
7096
return super.getNullValue(ctxt);
7197
} catch (IOException e) {
7298
throw JsonMappingException.from(ctxt, e.toString());
7399
}
74100
}
75101

102+
private IonType getIonContainerType() {
103+
if (_targetType != null) {
104+
Class<?> clazz = _targetType.getRawClass();
105+
if (IonStruct.class.isAssignableFrom(clazz)) return IonType.STRUCT;
106+
if (IonList.class.isAssignableFrom(clazz)) return IonType.LIST;
107+
if (IonSexp.class.isAssignableFrom(clazz)) return IonType.SEXP;
108+
}
109+
return null;
110+
}
111+
76112
@Override
77113
public AccessPattern getNullAccessPattern() {
78114
return AccessPattern.DYNAMIC;

Diff for: ion/src/test/java/com/fasterxml/jackson/dataformat/ion/ionvalue/IonValueDeserializerTest.java

+77-16
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
package com.fasterxml.jackson.dataformat.ion.ionvalue;
22

3-
import java.io.IOException;
4-
import java.util.*;
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
7+
import com.amazon.ion.IonSystem;
8+
import com.amazon.ion.IonValue;
9+
import com.amazon.ion.IonStruct;
510

6-
import com.amazon.ion.*;
7-
import com.amazon.ion.system.IonSystemBuilder;
811
import org.junit.jupiter.api.Test;
912

10-
import com.fasterxml.jackson.annotation.*;
13+
import com.amazon.ion.system.IonSystemBuilder;
14+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
15+
import com.fasterxml.jackson.annotation.JsonAnySetter;
16+
import com.fasterxml.jackson.annotation.JsonInclude;
17+
import com.fasterxml.jackson.annotation.JsonProperty;
18+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
1119
import com.fasterxml.jackson.databind.util.AccessPattern;
1220
import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
21+
import com.fasterxml.jackson.dataformat.ion.IonParser;
1322

14-
import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE;
1523
import static org.junit.jupiter.api.Assertions.assertEquals;
1624
import static org.junit.jupiter.api.Assertions.assertNull;
1725

@@ -65,7 +73,10 @@ static class IonValueData extends Data<IonValue> {
6573
}
6674

6775
private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
68-
private static final IonValueMapper ION_VALUE_MAPPER = new IonValueMapper(SYSTEM, SNAKE_CASE);
76+
private static final IonValueMapper ION_VALUE_MAPPER
77+
= new IonValueMapper(SYSTEM, PropertyNamingStrategies.SNAKE_CASE);
78+
private static final IonValueMapper ION_MAPPER_READ_NULL_DISABLED
79+
= (IonValueMapper) new IonValueMapper(SYSTEM, PropertyNamingStrategies.SNAKE_CASE).disable(IonParser.Feature.READ_NULL_AS_IONVALUE);
6980

7081
@Test
7182
public void shouldBeAbleToDeserialize() throws Exception {
@@ -91,24 +102,48 @@ public void shouldBeAbleToDeserializeIncludingNullList() throws Exception {
91102
assertEquals(ion("null.list"), data.getAllData().get("c"));
92103
}
93104

105+
@Test
106+
public void shouldBeAbleToDeserializeNullToIonNull() throws Exception {
107+
verifyNullDeserialization("{c:null}", SYSTEM.newNull(), null);
108+
}
109+
94110
@Test
95111
public void shouldBeAbleToDeserializeNullList() throws Exception {
96-
IonValue ion = ion("{c:null.list}");
112+
verifyNullDeserialization("{c:null.list}", SYSTEM.newNullList());
113+
}
97114

98-
IonValueData data = ION_VALUE_MAPPER.readValue(ion, IonValueData.class);
99115

100-
assertEquals(1, data.getAllData().size());
101-
assertEquals(SYSTEM.newNullList(), data.getAllData().get("c"));
102-
}
103116

104117
@Test
105118
public void shouldBeAbleToDeserializeNullStruct() throws Exception {
106-
IonValue ion = ion("{c:null.struct}");
119+
verifyNullDeserialization("{c:null.struct}", SYSTEM.newNullStruct());
120+
}
107121

108-
IonValueData data = ION_VALUE_MAPPER.readValue(ion, IonValueData.class);
122+
@Test
123+
public void shouldBeAbleToDeserializeNullSexp() throws Exception {
124+
verifyNullDeserialization("{c:null.sexp}", SYSTEM.newNullSexp());
125+
}
126+
127+
private void verifyNullDeserialization(String ionString, IonValue expected) throws Exception {
128+
verifyNullDeserialization(ionString, expected, expected);
129+
}
130+
131+
private void verifyNullDeserialization(String ionString, IonValue expected, IonValue expectedReadNullDisabled) throws Exception {
132+
verifyNullDeserialization(ION_VALUE_MAPPER, ionString, expected);
133+
verifyNullDeserialization(ION_MAPPER_READ_NULL_DISABLED, ionString, expectedReadNullDisabled);
134+
}
135+
136+
private void verifyNullDeserialization(IonValueMapper mapper, String ionString, IonValue expected) throws Exception {
137+
IonValueData data = mapper.readValue(ionString, IonValueData.class);
138+
139+
assertEquals(1, data.getAllData().size());
140+
assertEquals(expected, data.getAllData().get("c"));
141+
142+
IonValue ion = ion(ionString);
143+
data = mapper.readValue(ion, IonValueData.class);
109144

110145
assertEquals(1, data.getAllData().size());
111-
assertEquals(SYSTEM.newNullStruct(), data.getAllData().get("c"));
146+
assertEquals(expected, data.getAllData().get("c"));
112147
}
113148

114149
@Test
@@ -154,6 +189,22 @@ public void shouldBeAbleToSerializeAndDeserializePojo() throws Exception {
154189
assertEquals(source, result);
155190
}
156191

192+
@Test
193+
public void shouldBeAbleToSerializeAndDeserializeIonValueDataWithIncludeNonNull() throws Exception {
194+
IonValueData source = new IonValueData();
195+
source.put("a", SYSTEM.newInt(1));
196+
source.put("b", SYSTEM.newNull());
197+
source.put("c", null);
198+
IonValueMapper mapper = (IonValueMapper) ION_VALUE_MAPPER.copy().setSerializationInclusion(JsonInclude.Include.NON_NULL);
199+
200+
String data = mapper.writeValueAsString(source);
201+
assertEquals("{a:1,b:null}", data);
202+
// Now remove the null element for the comparison below.
203+
source.getAllData().remove("c");
204+
IonValueData result = mapper.readValue(data, IonValueData.class);
205+
assertEquals(source, result);
206+
}
207+
157208
@Test
158209
public void shouldBeAbleToSerializeAndDeserializeStringData() throws Exception {
159210
StringData source = new StringData();
@@ -162,7 +213,17 @@ public void shouldBeAbleToSerializeAndDeserializeStringData() throws Exception {
162213

163214
IonValue data = ION_VALUE_MAPPER.writeValueAsIonValue(source);
164215
StringData result = ION_VALUE_MAPPER.parse(data, StringData.class);
216+
assertEquals(source, result);
217+
}
218+
219+
@Test
220+
public void shouldBeAbleToSerializeAndDeserializeStringDataAsString() throws Exception {
221+
StringData source = new StringData();
222+
source.put("a", "1");
223+
source.put("b", null);
165224

225+
String data = ION_VALUE_MAPPER.writeValueAsString(source);
226+
StringData result = ION_VALUE_MAPPER.readValue(data, StringData.class);
166227
assertEquals(source, result);
167228
}
168229

@@ -180,7 +241,7 @@ static class MyBean {
180241
}
181242

182243
@Test
183-
public void testWithMissingProperty() throws IOException
244+
public void testWithMissingProperty() throws Exception
184245
{
185246
IonSystem ionSystem = IonSystemBuilder.standard().build();
186247
IonObjectMapper ionObjectMapper = IonObjectMapper.builder(ionSystem)

Diff for: release-notes/CREDITS-2.x

+4
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,7 @@ Cormac Redmond (@credmond)
381381
Manuel Sugawara (@sugmanue)
382382
* Contributed #568: Improve ASCII decoding performance for `CBORParser`
383383
(2.19.0)
384+
385+
Josh Curry (@seadbrane)
386+
* Reported, contributed fix for #571: Unable to deserialize a pojo with IonStruct
387+
(2.19.0)

Diff for: release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Active maintainers:
1414
=== Releases ===
1515
------------------------------------------------------------------------
1616

17+
#571: Unable to deserialize a pojo with IonStruct
18+
(reported, fix contributed by Josh C)
19+
1720
2.19.0-rc2 (07-Apr-2025)
1821

1922
#300: (smile) Floats are encoded with sign extension while doubles without

0 commit comments

Comments
 (0)