Skip to content

Commit c28903c

Browse files
authored
Fix #5958: apply View processing with external type id case too (#5959)
1 parent c14e4ba commit c28903c

4 files changed

Lines changed: 128 additions & 0 deletions

File tree

‎release-notes/CREDITS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ Omkhar Arasaratnam (@omkhar)
363363
[3.1.4]
364364
* Contributed fix for #5957: Improve `java.time.Month` deserialization validation
365365
[3.1.4]
366+
* Contributed fix for #5958: `@JsonView` ignored with
367+
`@JsonTypeInfo(include = As.EXTERNAL_PROPERTY)`
368+
[3.2.0]
366369

367370
Michael Orzechowski (@MikeBlink)
368371
* Reported #5941: `MapperBuilder.addModule()` does not recursively register transitive

‎release-notes/VERSION‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
210210
#5952: `@JsonIgnore` does not prevent passing of property to `@JsonAnySetter`
211211
(reported by Omkhar A)
212212
(fix by @cowtowncoder, w/ Claude code)
213+
#5958: `@JsonView` ignored with `@JsonTypeInfo(include = As.EXTERNAL_PROPERTY)`
214+
(fixed by Omkhar A)
213215
- Remove project-specific Android SDK compatibility level, use shared;
214216
no change level itself (stillAndroid SDK 34)
215217

‎src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,8 @@ protected Object deserializeUsingPropertyBasedWithExternalTypeId(JsonParser p, D
14101410
final ExternalTypeHandler ext = _externalTypeIdHandler.start();
14111411
final PropertyBasedCreator creator = _propertyBasedCreator;
14121412
PropertyValueBuffer buffer = creator.startBuilding(p, ctxt, _objectIdReader, false);
1413+
// [databind#5958]: capture active view so type-ID properties respect @JsonView restrictions
1414+
final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null;
14131415

14141416
for (JsonToken t = p.currentToken(); t == JsonToken.PROPERTY_NAME; t = p.nextToken()) {
14151417
String propName = p.currentName();
@@ -1444,6 +1446,12 @@ protected Object deserializeUsingPropertyBasedWithExternalTypeId(JsonParser p, D
14441446
int ix = _propNameMatcher.matchName(propName);
14451447
if (ix >= 0) {
14461448
SettableBeanProperty prop = _propsByIndex[ix];
1449+
// [databind#5958]: check view before storing external type ID so that a
1450+
// view-restricted type discriminator is not processed in other views.
1451+
if (activeView != null && !prop.visibleInView(activeView)) {
1452+
p.skipChildren();
1453+
continue;
1454+
}
14471455
// [databind#3045]: may have property AND be used as external type id:
14481456
// [databind#1329]: if so, and visible=false, skip buffering
14491457
if (t.isScalarValue()
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package tools.jackson.databind.jsontype.ext;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.*;
6+
7+
import tools.jackson.databind.MapperFeature;
8+
import tools.jackson.databind.ObjectMapper;
9+
import tools.jackson.databind.ObjectReader;
10+
import tools.jackson.databind.testutil.DatabindTestUtil;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
// [databind#5958]: When a POJO uses a property-based @JsonCreator together with
15+
// an EXTERNAL_PROPERTY type id whose `visible=true` settable property is
16+
// restricted by @JsonView, the deserializer must skip that type-id property in
17+
// views where it is not visible (instead of forwarding the type id into the
18+
// external type handler).
19+
public class ExternalTypeIdView5958Test extends DatabindTestUtil
20+
{
21+
static class Views {
22+
static class Public { }
23+
static class Internal { }
24+
}
25+
26+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
27+
@JsonSubTypes({
28+
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
29+
@JsonSubTypes.Type(value = Cat.class, name = "cat")
30+
})
31+
static abstract class Animal {
32+
public String name;
33+
}
34+
35+
@JsonTypeName("dog")
36+
static class Dog extends Animal { }
37+
38+
@JsonTypeName("cat")
39+
static class Cat extends Animal { }
40+
41+
// Default impl used when the type id is filtered out by view processing.
42+
@JsonTypeName("unknown")
43+
static class UnknownAnimal extends Animal { }
44+
45+
static class Container {
46+
// External type id, exposed (`visible=true`) as a settable property,
47+
// restricted to the Internal view.
48+
@JsonView(Views.Internal.class)
49+
public String petType;
50+
51+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
52+
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
53+
property = "petType",
54+
visible = true,
55+
defaultImpl = UnknownAnimal.class)
56+
@JsonSubTypes({
57+
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
58+
@JsonSubTypes.Type(value = Cat.class, name = "cat")
59+
})
60+
@JsonView(Views.Internal.class)
61+
public Animal pet;
62+
63+
public String label;
64+
65+
// Property-based creator forces the
66+
// deserializeUsingPropertyBasedWithExternalTypeId code path.
67+
@JsonCreator
68+
public Container(@JsonProperty("label") String label) {
69+
this.label = label;
70+
}
71+
}
72+
73+
private final ObjectMapper MAPPER = jsonMapperBuilder()
74+
// Properties without a @JsonView are visible in every view; ones
75+
// with @JsonView are only visible in (a subtype of) that view.
76+
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
77+
.build();
78+
79+
private static final String JSON = a2q(
80+
"{'label':'box','petType':'dog','pet':{'name':'Rex'}}");
81+
82+
@Test
83+
public void typeIdHonoredInVisibleView() throws Exception
84+
{
85+
ObjectReader reader = MAPPER.readerFor(Container.class)
86+
.withView(Views.Internal.class);
87+
Container c = reader.readValue(JSON);
88+
89+
assertEquals("box", c.label);
90+
assertEquals("dog", c.petType);
91+
assertNotNull(c.pet);
92+
assertInstanceOf(Dog.class, c.pet);
93+
assertEquals("Rex", c.pet.name);
94+
}
95+
96+
// The core [databind#5958] assertion: under a view where the type-id property is
97+
// not visible, the value in the JSON must NOT be forwarded into the
98+
// external type handler. Without the patch, `petType:"dog"` would be picked
99+
// up via `_propsByIndex`/`handleTypePropertyValue` even though the field is
100+
// restricted to a different view, and `pet` would be deserialized as `Dog`.
101+
// With the patch, the type-id property is skipped and `defaultImpl` kicks in.
102+
@Test
103+
public void typeIdSkippedInRestrictedView() throws Exception
104+
{
105+
ObjectReader reader = MAPPER.readerFor(Container.class)
106+
.withView(Views.Public.class);
107+
Container c = reader.readValue(JSON);
108+
109+
assertEquals("box", c.label);
110+
assertNull(c.petType, "petType is @JsonView-restricted; must be skipped under Public view");
111+
assertNotNull(c.pet);
112+
assertInstanceOf(UnknownAnimal.class, c.pet,
113+
"type id must not leak across views; defaultImpl should be used instead of Dog");
114+
}
115+
}

0 commit comments

Comments
 (0)