Skip to content

Commit 8ed9216

Browse files
authored
BAEL 7697 - How to Distinguish Between Field Absent vs Null in Jackson (#18230)
* BAEL 7697 - first draft * review 1 * bael 7697 - review 3 * bael 7697 - restoring public methods to avoid error java.lang.IllegalArgumentException: No serializer found for class com.baeldung.jackson.absentvsnull.JacksonAbsentVsNullUnitTest$Sample and no properties discovered to create BeanSerializer
1 parent a37a655 commit 8ed9216

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package com.baeldung.jackson.absentvsnull;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.io.Serializable;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Set;
13+
14+
import org.junit.jupiter.api.Test;
15+
16+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
17+
import com.fasterxml.jackson.core.JsonProcessingException;
18+
import com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.DeserializationFeature;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
22+
23+
class JacksonAbsentVsNullUnitTest {
24+
25+
static final TypeReference<Map<String, Serializable>> MAP_TYPE = new TypeReference<Map<String, Serializable>>() {
26+
};
27+
28+
@Test
29+
void whenSerializingWithDefaults_thenNullValuesIncluded() {
30+
Sample zeroArg = new Sample();
31+
Map<String, Serializable> map = new ObjectMapper().convertValue(zeroArg, MAP_TYPE);
32+
33+
assertEquals(zeroArg.getAmount(), map.get("amount"));
34+
assertTrue(map.containsKey("id"));
35+
assertNull(map.get("id"));
36+
}
37+
38+
@Test
39+
void whenDeserializingToMapWithDefaults_thenAbsentFieldsAreIgnored() throws JsonProcessingException {
40+
String json = """
41+
{
42+
"values": [2],
43+
"keys": []}
44+
""";
45+
Map<String, Serializable> map = new ObjectMapper().readValue(json, MAP_TYPE);
46+
47+
Set<String> keySet = map.keySet();
48+
assertEquals(2, keySet.size());
49+
assertTrue(keySet.containsAll(List.of("keys", "values")));
50+
}
51+
52+
@Test
53+
void whenDeserializingToMapWithDefaults_thenNullPrimitiveIsDefaulted() throws JsonProcessingException {
54+
String json = """
55+
{
56+
"amount": null
57+
}
58+
""";
59+
Sample sample = new ObjectMapper().readValue(json, Sample.class);
60+
61+
assertEquals(0, sample.getAmount());
62+
}
63+
64+
@Test
65+
void whenValidatingNullPrimitives_thenFailOnNullAmount() {
66+
ObjectMapper mapper = new ObjectMapper();
67+
mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
68+
69+
String json = """
70+
{
71+
"amount": null
72+
}
73+
""";
74+
75+
assertThrows(MismatchedInputException.class, () -> mapper.readValue(json, Sample.class));
76+
}
77+
78+
@Test
79+
void whenDeserializingToJavaWithDefaults_thenAbsentFieldsArePresent() throws JsonProcessingException {
80+
String json = """
81+
{
82+
"values": [2],
83+
"keys": []
84+
}
85+
""";
86+
Sample sample = new ObjectMapper().readValue(json, Sample.class);
87+
88+
assertEquals(new Sample().getAmount(), sample.getAmount());
89+
}
90+
91+
@Test
92+
void whenSerializingNonDefault_thenOnlyNonJavaDefaultsIncluded() {
93+
ObjectMapper mapper = new ObjectMapper();
94+
mapper.setSerializationInclusion(Include.NON_DEFAULT);
95+
96+
Sample zeroArg = new Sample();
97+
98+
Map<String, Serializable> map = mapper.convertValue(zeroArg, MAP_TYPE);
99+
100+
assertEquals(zeroArg.getAmount(), map.get("amount"));
101+
assertEquals(1, map.keySet()
102+
.size());
103+
}
104+
105+
@Test
106+
void whenPatchingNonNulls_thenNullsIgnored() throws JsonProcessingException {
107+
List<Integer> values = List.of(3);
108+
109+
Sample defaults = Sample.basic();
110+
111+
String json = """
112+
{
113+
"values": %s
114+
}
115+
""".formatted(values);
116+
117+
updateIgnoringNulls(json, defaults);
118+
119+
assertEquals(values, defaults.getValues());
120+
assertNotNull(defaults.getKeys());
121+
}
122+
123+
@Test
124+
void whenPatchingNonAbsent_thenNullsConsidered() throws JsonProcessingException {
125+
List<Integer> values = List.of(3);
126+
127+
Sample defaults = Sample.basic();
128+
129+
String json = """
130+
{
131+
"values": %s,
132+
"keys": null
133+
}
134+
""".formatted(values);
135+
136+
updateNonAbsent(json, defaults);
137+
138+
assertEquals(values, defaults.getValues());
139+
assertNull(defaults.getKeys());
140+
assertNotNull(defaults.getId());
141+
}
142+
143+
static void updateIgnoringNulls(String json, Sample current) throws JsonProcessingException {
144+
ObjectMapper mapper = new ObjectMapper();
145+
Sample update = mapper.readValue(json, Sample.class);
146+
mapper.setSerializationInclusion(Include.NON_DEFAULT);
147+
148+
if (update.getId() != null) {
149+
current.setId(update.getId());
150+
}
151+
152+
if (update.getName() != null) {
153+
current.setName(update.getName());
154+
}
155+
156+
current.setAmount(update.getAmount());
157+
158+
if (update.getKeys() != null) {
159+
current.setKeys(update.getKeys());
160+
}
161+
162+
if (update.getValues() != null) {
163+
current.setValues(update.getValues());
164+
}
165+
}
166+
167+
@SuppressWarnings("unchecked")
168+
static void updateNonAbsent(String json, Sample current) throws JsonProcessingException {
169+
ObjectMapper mapper = new ObjectMapper();
170+
Map<String, Serializable> update = mapper.readValue(json, MAP_TYPE);
171+
mapper.setSerializationInclusion(Include.NON_DEFAULT);
172+
173+
if (update.containsKey("id")) {
174+
current.setId((Long) update.get("id"));
175+
}
176+
177+
if (update.containsKey("name")) {
178+
current.setName((String) update.get("name"));
179+
}
180+
181+
if (update.containsKey("amount")) {
182+
current.setAmount((int) update.get("amount"));
183+
}
184+
185+
if (update.containsKey("keys")) {
186+
current.setKeys((List<String>) update.get("keys"));
187+
}
188+
189+
if (update.containsKey("values")) {
190+
current.setValues((List<Integer>) update.get("values"));
191+
}
192+
}
193+
194+
static class Sample {
195+
196+
private Long id;
197+
private String name;
198+
private int amount = 1;
199+
private List<String> keys;
200+
private List<Integer> values;
201+
202+
public Long getId() {
203+
return id;
204+
}
205+
206+
public void setId(Long id) {
207+
this.id = id;
208+
}
209+
210+
public String getName() {
211+
return name;
212+
}
213+
214+
public void setName(String name) {
215+
this.name = name;
216+
}
217+
218+
public int getAmount() {
219+
return amount;
220+
}
221+
222+
public void setAmount(int amount) {
223+
this.amount = amount;
224+
}
225+
226+
public List<String> getKeys() {
227+
return keys;
228+
}
229+
230+
public void setKeys(List<String> names) {
231+
this.keys = names;
232+
}
233+
234+
public List<Integer> getValues() {
235+
return values;
236+
}
237+
238+
public void setValues(List<Integer> values) {
239+
this.values = values;
240+
}
241+
242+
static Sample basic() {
243+
Sample defaults = new Sample();
244+
245+
List<String> keys = List.of("foo", "bar");
246+
List<Integer> values = List.of(1, 2);
247+
248+
defaults.setId(1l);
249+
defaults.setName("name");
250+
defaults.setAmount(3);
251+
defaults.setKeys(keys);
252+
defaults.setValues(values);
253+
254+
return defaults;
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)