Skip to content

Commit 44bc080

Browse files
authored
Merge pull request #1341 from ckuhn/master
Added FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY deser property
2 parents d4a2092 + 815b7a8 commit 44bc080

File tree

3 files changed

+280
-2
lines changed

3 files changed

+280
-2
lines changed

src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java

+11
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,17 @@ public enum DeserializationFeature implements ConfigFeature
244244
*/
245245
FAIL_ON_NULL_CREATOR_PROPERTIES(false),
246246

247+
/**
248+
* Feature that determines what happens when a property annotated with
249+
* {@link com.fasterxml.jackson.annotation.JsonTypeInfo.As#EXTERNAL_PROPERTY} is missing.
250+
* This is disabled by default, so that no error is thrown when a subtype property is
251+
* missing, unless the property is explicitly marked as `required`. If it is enabled, or
252+
* the property is marked as 'required' then a {@link JsonMappingException} will be thrown.
253+
*
254+
* @since 2.8
255+
*/
256+
FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY(false),
257+
247258
/**
248259
* Feature that determines whether Jackson code should catch
249260
* and wrap {@link Exception}s (but never {@link Error}s!)

src/main/java/com/fasterxml/jackson/databind/deser/impl/ExternalTypeHandler.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,13 @@ public Object complete(JsonParser p, DeserializationContext ctxt, Object bean)
165165
}
166166
} else if (_tokens[i] == null) {
167167
SettableBeanProperty prop = _properties[i].getProperty();
168-
ctxt.reportMappingException("Missing property '%s' for external type id '%s'",
169-
prop.getName(), _properties[i].getTypePropertyName());
168+
169+
if(prop.isRequired() ||
170+
ctxt.isEnabled(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY)) {
171+
ctxt.reportMappingException("Missing property '%s' for external type id '%s'",
172+
prop.getName(), _properties[i].getTypePropertyName());
173+
}
174+
return bean;
170175
}
171176
_deserializeAndSet(p, ctxt, bean, i, typeId);
172177
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package com.fasterxml.jackson.databind.jsontype;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
6+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
7+
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
8+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
9+
import com.fasterxml.jackson.databind.BaseMapTest;
10+
import com.fasterxml.jackson.databind.DeserializationFeature;
11+
import com.fasterxml.jackson.databind.JsonMappingException;
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
import org.junit.Rule;
14+
import org.junit.Test;
15+
import org.junit.rules.ExpectedException;
16+
17+
import java.io.IOException;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNull;
21+
import static org.junit.Assert.assertSame;
22+
23+
public class TestSubtypesExternalPropertyMissingProperty {
24+
@Rule
25+
public ExpectedException thrown = ExpectedException.none();
26+
27+
/**
28+
* Base class - external property for Fruit subclasses.
29+
*/
30+
static class Box {
31+
public String type;
32+
@JsonTypeInfo(use = Id.NAME, include = As.EXTERNAL_PROPERTY, property = "type")
33+
public Fruit fruit;
34+
35+
public Box() {
36+
}
37+
38+
public Box(String type, Fruit fruit) {
39+
this.type = type;
40+
this.fruit = fruit;
41+
}
42+
}
43+
44+
/**
45+
* Base class that requires the property to be present.
46+
*/
47+
static class ReqBox {
48+
public String type;
49+
@JsonTypeInfo(use = Id.NAME, include = As.EXTERNAL_PROPERTY, property = "type")
50+
@JsonProperty(required = true)
51+
public Fruit fruit;
52+
53+
public ReqBox() {
54+
}
55+
56+
public ReqBox(String type, Fruit fruit) {
57+
this.type = type;
58+
this.fruit = fruit;
59+
}
60+
}
61+
62+
@JsonSubTypes({
63+
@Type(value = Apple.class, name = "apple"),
64+
@Type(value = Orange.class, name = "orange")
65+
})
66+
static abstract class Fruit {
67+
public String name;
68+
69+
public Fruit() {
70+
}
71+
72+
protected Fruit(String n) {
73+
name = n;
74+
}
75+
}
76+
77+
static class Apple extends Fruit {
78+
public int seedCount;
79+
80+
public Apple() {
81+
}
82+
83+
public Apple(String name, int b) {
84+
super(name);
85+
seedCount = b;
86+
}
87+
}
88+
89+
static class Orange extends Fruit {
90+
public String color;
91+
92+
public Orange() {
93+
}
94+
95+
public Orange(String name, String c) {
96+
super(name);
97+
color = c;
98+
}
99+
}
100+
101+
private final ObjectMapper MAPPER = new ObjectMapper();
102+
103+
/*
104+
/**********************************************************
105+
/* Mock data
106+
/**********************************************************
107+
*/
108+
109+
private static final Orange orange = new Orange("Orange", "orange");
110+
private static final Box orangeBox = new Box("orange", orange);
111+
private static final String orangeBoxJson = "{\"type\":\"orange\",\"fruit\":{\"name\":\"Orange\",\"color\":\"orange\"}}";
112+
private static final String orangeBoxNullJson = "{\"type\":\"orange\",\"fruit\":null}}";
113+
private static final String orangeBoxEmptyJson = "{\"type\":\"orange\",\"fruit\":{}}}";
114+
private static final String orangeBoxMissingJson = "{\"type\":\"orange\"}}";
115+
116+
private static final Apple apple = new Apple("Apple", 16);
117+
private static Box appleBox = new Box("apple", apple);
118+
private static final String appleBoxJson = "{\"type\":\"apple\",\"fruit\":{\"name\":\"Apple\",\"seedCount\":16}}";
119+
private static final String appleBoxNullJson = "{\"type\":\"apple\",\"fruit\":null}";
120+
private static final String appleBoxEmptyJson = "{\"type\":\"apple\",\"fruit\":{}}";
121+
private static final String appleBoxMissingJson = "{\"type\":\"apple\"}";
122+
123+
/*
124+
/**********************************************************
125+
/* Unit tests
126+
/**********************************************************
127+
*/
128+
129+
/**
130+
* Deserialization tests for external type id property present
131+
*/
132+
@Test
133+
public void testDeserializationPresent() throws Exception {
134+
MAPPER.disable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
135+
checkOrangeBox();
136+
checkAppleBox();
137+
138+
MAPPER.enable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
139+
checkOrangeBox();
140+
checkAppleBox();
141+
}
142+
143+
/**
144+
* Deserialization tests for external type id property null
145+
*/
146+
@Test
147+
public void testDeserializationNull() throws Exception {
148+
MAPPER.disable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
149+
checkOrangeBoxNull(orangeBoxNullJson);
150+
checkAppleBoxNull(appleBoxNullJson);
151+
152+
MAPPER.enable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
153+
checkOrangeBoxNull(orangeBoxNullJson);
154+
checkAppleBoxNull(appleBoxNullJson);
155+
}
156+
157+
/**
158+
* Deserialization tests for external type id property empty
159+
*/
160+
@Test
161+
public void testDeserializationEmpty() throws Exception {
162+
MAPPER.disable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
163+
checkOrangeBoxEmpty(orangeBoxEmptyJson);
164+
checkAppleBoxEmpty(appleBoxEmptyJson);
165+
166+
MAPPER.enable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
167+
checkOrangeBoxEmpty(orangeBoxEmptyJson);
168+
checkAppleBoxEmpty(appleBoxEmptyJson);
169+
}
170+
171+
/**
172+
* Deserialization tests for external type id property missing
173+
*/
174+
@Test
175+
public void testDeserializationMissing() throws Exception {
176+
MAPPER.disable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
177+
checkOrangeBoxNull(orangeBoxMissingJson);
178+
checkAppleBoxNull(appleBoxMissingJson);
179+
180+
MAPPER.enable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
181+
checkBoxJsonMappingException(orangeBoxMissingJson);
182+
checkBoxJsonMappingException(appleBoxMissingJson);
183+
}
184+
185+
/**
186+
* Deserialization tests for external type id required property missing
187+
*/
188+
@Test
189+
public void testDeserializationMissingRequired() throws Exception {
190+
MAPPER.disable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
191+
checkReqBoxJsonMappingException(orangeBoxMissingJson);
192+
checkReqBoxJsonMappingException(appleBoxMissingJson);
193+
194+
MAPPER.enable(DeserializationFeature.FAIL_ON_EXTERNAL_TYPE_ID_MISSING_PROPERTY);
195+
checkReqBoxJsonMappingException(orangeBoxMissingJson);
196+
checkReqBoxJsonMappingException(appleBoxMissingJson);
197+
}
198+
199+
private void checkOrangeBox() throws Exception {
200+
Box deserOrangeBox = MAPPER.readValue(orangeBoxJson, Box.class);
201+
assertEquals(orangeBox.type, deserOrangeBox.type);
202+
203+
Fruit deserOrange = deserOrangeBox.fruit;
204+
assertSame(Orange.class, deserOrange.getClass());
205+
assertEquals(orange.name, deserOrange.name);
206+
assertEquals(orange.color, ((Orange) deserOrange).color);
207+
}
208+
209+
private void checkAppleBox() throws Exception {
210+
Box deserAppleBox = MAPPER.readValue(appleBoxJson, Box.class);
211+
assertEquals(appleBox.type, deserAppleBox.type);
212+
213+
Fruit deserApple = deserAppleBox.fruit;
214+
assertSame(Apple.class, deserApple.getClass());
215+
assertEquals(apple.name, deserApple.name);
216+
assertEquals(apple.seedCount, ((Apple) deserApple).seedCount);
217+
}
218+
219+
private void checkOrangeBoxEmpty(String json) throws Exception {
220+
Box deserOrangeBox = MAPPER.readValue(json, Box.class);
221+
assertEquals(orangeBox.type, deserOrangeBox.type);
222+
223+
Fruit deserOrange = deserOrangeBox.fruit;
224+
assertSame(Orange.class, deserOrange.getClass());
225+
assertNull(deserOrange.name);
226+
assertNull(((Orange) deserOrange).color);
227+
}
228+
229+
private void checkAppleBoxEmpty(String json) throws Exception {
230+
Box deserAppleBox = MAPPER.readValue(json, Box.class);
231+
assertEquals(appleBox.type, deserAppleBox.type);
232+
233+
Fruit deserApple = deserAppleBox.fruit;
234+
assertSame(Apple.class, deserApple.getClass());
235+
assertNull(deserApple.name);
236+
assertEquals(0, ((Apple) deserApple).seedCount);
237+
}
238+
239+
private void checkOrangeBoxNull(String json) throws Exception {
240+
Box deserOrangeBox = MAPPER.readValue(json, Box.class);
241+
assertEquals(orangeBox.type, deserOrangeBox.type);
242+
assertNull(deserOrangeBox.fruit);
243+
}
244+
245+
private void checkAppleBoxNull(String json) throws Exception {
246+
Box deserAppleBox = MAPPER.readValue(json, Box.class);
247+
assertEquals(appleBox.type, deserAppleBox.type);
248+
assertNull(deserAppleBox.fruit);
249+
}
250+
251+
private void checkBoxJsonMappingException(String json) throws Exception {
252+
thrown.expect(JsonMappingException.class);
253+
thrown.expectMessage("Missing property 'fruit' for external type id 'type'");
254+
MAPPER.readValue(json, Box.class);
255+
}
256+
257+
private void checkReqBoxJsonMappingException(String json) throws Exception {
258+
thrown.expect(JsonMappingException.class);
259+
thrown.expectMessage("Missing property 'fruit' for external type id 'type'");
260+
MAPPER.readValue(json, ReqBox.class);
261+
}
262+
}

0 commit comments

Comments
 (0)