ViewCreatorDeserializationBypassTest.java
package tools.jackson.databind.views;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.annotation.JsonPOJOBuilder;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verifies that an active {@code @JsonView} is honored across every
* property-based-creator deserialization path -- i.e. that a
* {@code @JsonView(Admin)} property cannot be populated while reading under a
* less-privileged view by reordering JSON fields or by routing through a
* specialized code path.
*<p>
* Consolidates the per-path regression tests for [databind#5969] / [databind#5971]
* and their builder/external-type-id/record-update siblings.
* Each {@link Nested} class targets one deserialization
* path; all share the {@code PublicView}/{@code AdminView} hierarchy and the same
* "negative control + bypass" structure.
*/
public class ViewCreatorDeserializationBypassTest extends DatabindTestUtil
{
static class PublicView {}
static class AdminView extends PublicView {}
private final ObjectMapper MAPPER = newJsonMapper();
/**
* [databind#5969]: regression where setterless collection properties enrolled
* in the merging buffer path of {@code BeanDeserializer._deserializeUsingPropertyBased()}
* skipped the {@code visibleInView} check.
*/
@Nested
class SetterlessCreator5969
{
static class CreatorBean {
@JsonView(PublicView.class)
private String name;
// Setterless collection -> SetterlessProperty; @JsonView(Admin) means
// it must be invisible in PublicView.
@JsonView(AdminView.class)
private final List<String> roles = new ArrayList<>();
@JsonCreator
public CreatorBean(@JsonProperty("name") @JsonView(PublicView.class) String name) {
this.name = name;
}
@JsonView(PublicView.class)
public String getName() { return name; }
@JsonView(AdminView.class)
public List<String> getRoles() { return roles; }
}
@Test
public void adminViewPopulatesBoth() throws Exception {
CreatorBean result = MAPPER.readerWithView(AdminView.class)
.forType(CreatorBean.class)
.readValue("""
{"name":"alice","roles":["admin"]}
""");
assertEquals("alice", result.getName());
assertEquals(List.of("admin"), result.getRoles());
}
// roles BEFORE the creator property "name" forces the regular-property buffering path.
@Test
public void rolesBeforeCreatorHonorsView() throws Exception {
CreatorBean result = MAPPER.readerWithView(PublicView.class)
.forType(CreatorBean.class)
.readValue("""
{"roles":["admin"],"name":"alice"}
""");
assertEquals("alice", result.getName());
assertTrue(result.getRoles().isEmpty(),
"[databind#5969] setterless @JsonView(AdminView) populated in PublicView: " + result.getRoles());
}
@Test
public void rolesAfterCreatorHonorsView() throws Exception {
CreatorBean result = MAPPER.readerWithView(PublicView.class)
.forType(CreatorBean.class)
.readValue("""
{"name":"alice","roles":["admin"]}
""");
assertEquals("alice", result.getName());
assertTrue(result.getRoles().isEmpty(),
"[databind#5969] setterless @JsonView(AdminView) populated in PublicView: " + result.getRoles());
}
}
/**
* [databind#5971]: {@code UnwrappedPropertyHandler.processUnwrappedCreatorProperties()}
* iterated creator properties without checking {@code visibleInView}, so an
* {@code @JsonView(Admin)} {@code @JsonUnwrapped} constructor parameter could be
* populated under a less-privileged view.
*/
@Nested
class UnwrappedCreatorParam5971
{
static class Address {
@JsonView(AdminView.class)
public String street;
@JsonView(PublicView.class)
public String city;
}
static class UserBean {
@JsonView(PublicView.class)
public final String name;
@JsonView(AdminView.class)
public final Address address;
@JsonCreator
public UserBean(
@JsonProperty("name") @JsonView(PublicView.class) String name,
@JsonView(AdminView.class) @JsonUnwrapped Address address) {
this.name = name;
this.address = address;
}
}
@Test
public void adminViewPopulatesBoth() throws Exception {
UserBean result = MAPPER.readerWithView(AdminView.class)
.forType(UserBean.class)
.readValue("""
{"name":"alice","street":"1 Main St","city":"Springfield"}
""");
assertEquals("alice", result.name);
assertNotNull(result.address);
assertEquals("1 Main St", result.address.street);
assertEquals("Springfield", result.address.city);
}
@Test
public void unwrappedCreatorParamHonorsView() throws Exception {
UserBean result = MAPPER.readerWithView(PublicView.class)
.forType(UserBean.class)
.readValue("""
{"name":"alice","street":"1 Main St","city":"Springfield"}
""");
assertEquals("alice", result.name);
assertNull(result.address,
"[databind#5971] @JsonView(AdminView) @JsonUnwrapped creator param populated in PublicView: "
+ result.address);
}
}
/**
* GHSA-x94j-jhxw-j455 (builder path): {@code BuilderBasedDeserializer._deserializeUsingPropertyBased()}
* skipped the view check on the regular-property buffering branch, so a
* {@code @JsonView(Admin)} property ordered between the creator's parameters
* could be populated under a non-Admin view ([databind#5969] sibling).
*/
@Nested
class BuilderBased
{
@JsonDeserialize(builder = User.Builder.class)
static class User {
@JsonView(PublicView.class) public final String name;
@JsonView(PublicView.class) public final String city;
@JsonView(AdminView.class) public final String password;
private User(String n, String c, String p) {
this.name = n; this.city = c; this.password = p;
}
@JsonPOJOBuilder(withPrefix = "")
static class Builder {
private String name, city, password;
@JsonCreator
public Builder(@JsonProperty("name") @JsonView(PublicView.class) String n,
@JsonProperty("city") @JsonView(PublicView.class) String c) {
this.name = n; this.city = c;
}
@JsonView(PublicView.class) public Builder name(String n) { this.name = n; return this; }
@JsonView(PublicView.class) public Builder city(String c) { this.city = c; return this; }
@JsonView(AdminView.class) public Builder password(String p) { this.password = p; return this; }
public User build() { return new User(name, city, password); }
}
}
@Test
public void adminViewPopulatesAll() throws Exception {
User u = MAPPER.readerWithView(AdminView.class).forType(User.class)
.readValue("""
{"name":"alice","password":"secret","city":"NY"}
""");
assertEquals("alice", u.name);
assertEquals("NY", u.city);
assertEquals("secret", u.password);
}
// password BETWEEN the two creator props -> regular-property buffering branch.
@Test
public void regularPropertyInCreatorWindowHonorsView() throws Exception {
User u = MAPPER.readerWithView(PublicView.class).forType(User.class)
.readValue("""
{"name":"alice","password":"BYPASS","city":"NY"}
""");
assertEquals("alice", u.name);
assertEquals("NY", u.city);
assertNull(u.password,
"[databind#5969] @JsonView(AdminView) builder property populated in PublicView (creator window): "
+ u.password);
}
@Test
public void regularPropertyAfterCreatorHonorsView() throws Exception {
User u = MAPPER.readerWithView(PublicView.class).forType(User.class)
.readValue("""
{"name":"alice","city":"NY","password":"BYPASS"}
""");
assertEquals("alice", u.name);
assertEquals("NY", u.city);
assertNull(u.password,
"[databind#5969] @JsonView(AdminView) builder property populated in PublicView: " + u.password);
}
}
/**
* GHSA-x94j-jhxw-j455 (BeanDeserializer unwrapped path):
* {@code deserializeUsingPropertyBasedWithUnwrapped()} skipped the view check on
* both the creator-property ([databind#5971]) and regular-property
* ([databind#5969]) branches when an {@code @JsonUnwrapped} member forced that path.
*/
@Nested
class UnwrappedPojo
{
static class Address {
@JsonView(PublicView.class)
public String city;
}
static class UserBean {
@JsonView(PublicView.class)
public final String name;
// Creator parameter, view-restricted -> creator branch ([databind#5971]).
@JsonView(AdminView.class)
public final String secret;
// Non-creator, view-restricted -> regular-property branch ([databind#5969]).
@JsonView(AdminView.class)
public String password;
@JsonView(PublicView.class)
@JsonUnwrapped
public Address address;
@JsonCreator
public UserBean(@JsonProperty("name") @JsonView(PublicView.class) String name,
@JsonProperty("secret") @JsonView(AdminView.class) String secret) {
this.name = name;
this.secret = secret;
}
}
@Test
public void adminViewPopulatesAll() throws Exception {
UserBean result = MAPPER.readerWithView(AdminView.class).forType(UserBean.class)
.readValue("""
{"name":"alice","secret":"sssh","password":"secret","city":"NY"}
""");
assertEquals("alice", result.name);
assertEquals("sssh", result.secret);
assertEquals("secret", result.password);
assertNotNull(result.address);
assertEquals("NY", result.address.city);
}
@Test
public void regularPropertyHonorsView() throws Exception {
UserBean result = MAPPER.readerWithView(PublicView.class).forType(UserBean.class)
.readValue("""
{"name":"alice","password":"BYPASS","city":"NY"}
""");
assertEquals("alice", result.name);
assertNotNull(result.address);
assertEquals("NY", result.address.city);
assertNull(result.password,
"[databind#5969] @JsonView(AdminView) property populated in PublicView (unwrapped path): "
+ result.password);
}
@Test
public void creatorPropertyHonorsView() throws Exception {
UserBean result = MAPPER.readerWithView(PublicView.class).forType(UserBean.class)
.readValue("""
{"name":"alice","secret":"BYPASS","city":"NY"}
""");
assertEquals("alice", result.name);
assertNotNull(result.address);
assertEquals("NY", result.address.city);
assertNull(result.secret,
"[databind#5971] @JsonView(AdminView) creator param populated in PublicView (unwrapped path): "
+ result.secret);
}
}
/**
* GHSA-x94j-jhxw-j455 (external-type-id path):
* {@code deserializeUsingPropertyBasedWithExternalTypeId()} skipped the view check
* on creator-property ([databind#5971]) and regular-property ([databind#5969])
* branches. (The nested polymorphic value content is not deep-checked: its binding
* under property-based creators is an unrelated existing quirk.)
*/
@Nested
class ExternalTypeId
{
static final String FULL_JSON = """
{"name":"alice","secret":"sssh","password":"pw","valueType":"string","value":{"v":"x"}}
""";
public interface Value { }
@JsonTypeName("string")
static class StringValue implements Value {
public String v;
}
static class Envelope {
@JsonView(PublicView.class)
public final String name;
@JsonView(AdminView.class)
public final String secret;
@JsonView(AdminView.class)
public String password;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "valueType")
@JsonSubTypes({ @JsonSubTypes.Type(StringValue.class) })
@JsonView(PublicView.class)
public final Value value;
@JsonCreator
public Envelope(@JsonProperty("name") @JsonView(PublicView.class) String name,
@JsonProperty("secret") @JsonView(AdminView.class) String secret,
@JsonProperty("value") @JsonView(PublicView.class) Value value) {
this.name = name;
this.secret = secret;
this.value = value;
}
}
@Test
public void adminViewPopulatesRestricted() throws Exception {
Envelope result = MAPPER.readerWithView(AdminView.class).forType(Envelope.class)
.readValue(FULL_JSON);
assertEquals("alice", result.name);
assertEquals("sssh", result.secret);
assertEquals("pw", result.password);
assertTrue(result.value instanceof StringValue);
}
@Test
public void regularPropertyHonorsView() throws Exception {
Envelope result = MAPPER.readerWithView(PublicView.class).forType(Envelope.class)
.readValue(FULL_JSON);
assertEquals("alice", result.name);
assertTrue(result.value instanceof StringValue);
assertNull(result.password,
"[databind#5969] @JsonView(AdminView) property populated in PublicView (external-type-id): "
+ result.password);
}
@Test
public void creatorPropertyHonorsView() throws Exception {
Envelope result = MAPPER.readerWithView(PublicView.class).forType(Envelope.class)
.readValue(FULL_JSON);
assertEquals("alice", result.name);
assertTrue(result.value instanceof StringValue);
assertNull(result.secret,
"[databind#5971] @JsonView(AdminView) creator param populated in PublicView (external-type-id): "
+ result.secret);
}
}
/**
* [databind#5971] / [databind#5966] (record-update path):
* {@code BeanDeserializer._deserializeRecordForUpdate()} (new in 3.1) must not
* overwrite a {@code @JsonView(Admin)} record component from JSON while updating
* under a less-privileged view.
*/
public record UserRecord(
@JsonView(PublicView.class) String name,
@JsonView(AdminView.class) String role) {
}
@Nested
class RecordUpdate
{
@Test
public void adminViewAllowsOverride() throws Exception {
UserRecord existing = new UserRecord("alice", "user");
UserRecord result = MAPPER.readerWithView(AdminView.class)
.forType(UserRecord.class)
.withValueToUpdate(existing)
.readValue("""
{"name":"alice2","role":"admin"}
""");
assertEquals("alice2", result.name());
assertEquals("admin", result.role());
}
@Test
public void creatorPropertyHonorsViewOnUpdate() throws Exception {
UserRecord existing = new UserRecord("alice", "user");
UserRecord result = MAPPER.readerWithView(PublicView.class)
.forType(UserRecord.class)
.withValueToUpdate(existing)
.readValue("""
{"name":"alice2","role":"HACKED"}
""");
assertEquals("alice2", result.name());
assertEquals("user", result.role(),
"[databind#5971] @JsonView(AdminView) record component overwritten in PublicView (record-update): "
+ result.role());
}
}
}