Skip to content

Commit c38dc24

Browse files
authored
Fix handling of @JsonApplyView (#873)
1 parent aff0220 commit c38dc24

3 files changed

Lines changed: 132 additions & 4 deletions

File tree

release-notes/VERSION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Version: 3.x (for earlier see VERSION-2.x)
77

88
3.3.0 (not yet released)
99

10-
No changes since 3.2
10+
#873: Fix handling of `@JsonApplyView`
11+
(fix by @cowtowncoder, w/ Claude code)
1112

1213
3.2.1 (not yet released)
1314

src/main/java/tools/jackson/dataformat/xml/ser/XmlBeanPropertyWriter.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import javax.xml.namespace.QName;
44

5+
import com.fasterxml.jackson.annotation.JsonApplyView;
6+
57
import tools.jackson.core.JsonGenerator;
68
import tools.jackson.databind.*;
79
import tools.jackson.databind.ser.*;
@@ -184,16 +186,30 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte
184186
// [dataformat-xml#27]: Use wrapped name (inner element name), not property
185187
// name which may be the wrapper name after introspector conflict resolution
186188
g.writeName(_wrappedQName.getLocalPart());
187-
if (_typeSerializer == null) {
188-
ser.serialize(value, g, ctxt);
189+
// 18-Jun-2026, tatu: Need to apply active View, same as
190+
// `BeanPropertyWriter.serializeAsProperty()` does
191+
if (_applyView == null) {
192+
_serialize(value, g, ctxt, ser);
189193
} else {
190-
ser.serializeWithType(value, g, ctxt, _typeSerializer);
194+
final ValueSerializer<Object> actualSer = ser;
195+
ctxt.withActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null,
196+
() -> _serialize(value, g, ctxt, actualSer));
191197
}
192198
if (xmlGen != null) {
193199
xmlGen.finishWrappedValue(_wrapperQName, _wrappedQName);
194200
}
195201
}
196202

203+
private void _serialize(Object value, JsonGenerator g, SerializationContext ctxt,
204+
ValueSerializer<Object> ser)
205+
{
206+
if (_typeSerializer == null) {
207+
ser.serialize(value, g, ctxt);
208+
} else {
209+
ser.serializeWithType(value, g, ctxt, _typeSerializer);
210+
}
211+
}
212+
197213
/**
198214
* Check if the runtime value is a Collection, array, or Iterable
199215
* (i.e. something that should get wrapper element handling).
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package tools.jackson.dataformat.xml.ser;
2+
3+
import java.util.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.annotation.JsonApplyView;
8+
import com.fasterxml.jackson.annotation.JsonView;
9+
10+
import tools.jackson.databind.JsonNode;
11+
import tools.jackson.dataformat.xml.XmlMapper;
12+
import tools.jackson.dataformat.xml.XmlTestUtil;
13+
import tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
14+
import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty;
15+
import tools.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
19+
/**
20+
* Tests for [dataformat-xml#873]: {@code @JsonApplyView} (i.e. the {@code _applyView}
21+
* mechanism) must be honored for Collection/array properties, both wrapped and unwrapped,
22+
* so that the forced View propagates to the collection's element beans -- same as
23+
* {@code BeanPropertyWriter.serializeAsProperty()} does for scalar properties.
24+
*/
25+
public class ListApplyView873Test extends XmlTestUtil
26+
{
27+
static class Views {
28+
static class Public {}
29+
static class Internal extends Public {}
30+
}
31+
32+
static class Item {
33+
@JsonView(Views.Public.class)
34+
public String pub = "A";
35+
@JsonView(Views.Internal.class)
36+
public String internalOnly = "SECRET";
37+
}
38+
39+
// Wrapped list with a forced View (Public)
40+
@JacksonXmlRootElement(localName = "Bean")
41+
static class WrappedForcedBean {
42+
@JsonApplyView(Views.Public.class)
43+
@JacksonXmlElementWrapper(localName = "items")
44+
@JacksonXmlProperty(localName = "item")
45+
public List<Item> items = new ArrayList<>(Arrays.asList(new Item()));
46+
}
47+
48+
// Wrapped list with View processing explicitly disabled (NONE)
49+
@JacksonXmlRootElement(localName = "Bean")
50+
static class WrappedNoneBean {
51+
// `@JsonView` so the property itself survives an active Public view;
52+
// `@JsonApplyView(NONE)` then disables View filtering for the elements.
53+
@JsonView(Views.Public.class)
54+
@JsonApplyView(JsonApplyView.NONE.class)
55+
@JacksonXmlElementWrapper(localName = "items")
56+
@JacksonXmlProperty(localName = "item")
57+
public List<Item> items = new ArrayList<>(Arrays.asList(new Item()));
58+
}
59+
60+
// Unwrapped list with a forced View (Public)
61+
@JacksonXmlRootElement(localName = "Bean")
62+
static class UnwrappedForcedBean {
63+
@JsonApplyView(Views.Public.class)
64+
@JacksonXmlElementWrapper(useWrapping = false)
65+
@JacksonXmlProperty(localName = "item")
66+
public List<Item> items = new ArrayList<>(Arrays.asList(new Item()));
67+
}
68+
69+
private final XmlMapper MAPPER = new XmlMapper();
70+
71+
// Wrapped: @JsonApplyView(Public) forces Public view onto elements even with no
72+
// active view set -> 'pub' kept, Internal-only 'internalOnly' excluded.
73+
@Test
74+
public void testForcedViewWrapped() throws Exception
75+
{
76+
JsonNode item = itemNode(MAPPER.writeValueAsString(new WrappedForcedBean()));
77+
assertEquals("A", item.path("pub").asString(null));
78+
assertTrue(item.path("internalOnly").isMissingNode(),
79+
"Internal-only property must be excluded by forced Public view: " + item);
80+
}
81+
82+
// Wrapped: @JsonApplyView(NONE) disables view filtering for elements, so even with
83+
// an active Public view BOTH properties are written.
84+
@Test
85+
public void testNoneViewWrapped() throws Exception
86+
{
87+
String xml = MAPPER.writerWithView(Views.Public.class)
88+
.writeValueAsString(new WrappedNoneBean());
89+
JsonNode item = itemNode(xml);
90+
assertEquals("A", item.path("pub").asString(null));
91+
assertEquals("SECRET", item.path("internalOnly").asString(null),
92+
"@JsonApplyView(NONE) must disable view filtering for elements: " + item);
93+
}
94+
95+
// Unwrapped list goes through a different writer path; verify it honors the forced
96+
// view too (regression guard, since the forced view is the whole point of #873).
97+
@Test
98+
public void testForcedViewUnwrapped() throws Exception
99+
{
100+
JsonNode item = MAPPER.readTree(MAPPER.writeValueAsString(new UnwrappedForcedBean()))
101+
.path("item");
102+
assertEquals("A", item.path("pub").asString(null));
103+
assertTrue(item.path("internalOnly").isMissingNode(),
104+
"Internal-only property must be excluded by forced Public view (unwrapped): " + item);
105+
}
106+
107+
// For wrapped output: <Bean><items><item>...</item></items></Bean>
108+
private JsonNode itemNode(String xml) {
109+
return MAPPER.readTree(xml).path("items").path("item");
110+
}
111+
}

0 commit comments

Comments
 (0)