ViewsWithCreatorDeserTest.java
package tools.jackson.databind.views;
import java.util.EnumMap;
import java.util.HashMap;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.*;
import tools.jackson.databind.*;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ViewsWithCreatorDeserTest extends DatabindTestUtil
{
static class View { }
static class View1 extends View { }
static class View2 extends View { }
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
getterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
static class ObjWithCreator {
public String a;
@JsonView(View1.class)
public String b;
@JsonView(View2.class)
public String c;
public ObjWithCreator() { }
@JsonCreator
public ObjWithCreator(@JsonProperty("a") String a, @JsonProperty("b") String b, @JsonProperty("c") String c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public String toString() {
return "%s-%s-%s".formatted(a, b, c);
}
}
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
getterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
static class ObjWithoutCreator {
public String a;
@JsonView(View1.class)
public String b;
@JsonView(View2.class)
public String c;
public ObjWithoutCreator() { }
public ObjWithoutCreator(String a, String b, String c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public String toString() {
return "%s-%s-%s".formatted(a, b, c);
}
}
// Ensure `MapperFeature.DEFAULT_VIEW_INCLUSION` is enabled
// (its default differs b/w Jackson 2.x and 3.x)
private final ObjectMapper MAPPER = jsonMapperBuilder()
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.build();
private final ObjectMapper FAIL_ON_MISSING_MAPPER = jsonMapperBuilder()
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
.build();
private final ObjectMapper NO_DEFAULT_INCLUSION_MAPPER = jsonMapperBuilder()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.build();
// [databind#1172]
@Test
public void testWithJsonCreator() throws Exception
{
ObjectReader reader = MAPPER.readerFor(ObjWithCreator.class).withView(View1.class);
// serialize first,
String json = MAPPER.writeValueAsString(new ObjWithCreator("a", "b", "c"));
// then back
assertEquals("a-b-null", reader.readValue(json).toString());
}
// [databind#1172]
@Test
public void testWithoutJsonCreator() throws Exception
{
ObjectReader reader = MAPPER.readerFor(ObjWithoutCreator.class)
.without(DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES)
.withView(View1.class);
// serialize first,
String json = MAPPER.writeValueAsString(new ObjWithoutCreator("a", "b", "c"));
// then back
assertEquals("a-b-null", reader.readValue(json).toString());
}
/*
/**********************************************************************
/* [databind#6261]: `@JsonView` on properties-based Enum Creator parameters
/**********************************************************************
*/
// All Enums below have the same constants, which tell which creator parameters
// were actually passed a value (as opposed to being hidden by the active view):
// `NONE` neither, `A_ONLY` only "a", `B_ONLY` only "b", `BOTH` both.
private static int seen(Object a, Object b) {
return ((a == null) ? 0 : 1) + ((b == null) ? 0 : 2);
}
enum ParamViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static ParamViewEnum create(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View2.class) String b) {
return values()[seen(a, b)];
}
}
// View on Enum type applies to parameters without explicit view
@JsonView(View2.class)
enum TypeViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static TypeViewEnum create(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") String b) {
return values()[seen(a, b)];
}
}
// No views: all parameters always visible
enum NoViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static NoViewEnum create(@JsonProperty("a") String a,
@JsonProperty("b") String b) {
return values()[seen(a, b)];
}
}
// Parameter view is a super-type of the active view
enum SuperViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static SuperViewEnum create(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View.class) String b) {
return values()[seen(a, b)];
}
}
// Enum whose view is set via mix-in, not annotations on the Enum itself
enum MixInViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static MixInViewEnum create(@JsonProperty("a") String a,
@JsonProperty("b") String b) {
return values()[seen(a, b)];
}
}
static abstract class MixInViewEnumMixIn {
@JsonCreator
static MixInViewEnum create(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View2.class) String b) {
return null;
}
}
// "a" is required but only visible in View1
enum RequiredViewEnum {
NONE, A_ONLY, B_ONLY, BOTH;
@JsonCreator
static RequiredViewEnum create(
@JsonProperty(value = "a", required = true) @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View2.class) String b) {
return values()[seen(a, b)];
}
}
// Enum as a property of a POJO, with the property itself visible only in View1
static class EnumHolder {
@JsonView(View1.class)
public ParamViewEnum param;
@JsonView(View1.class)
public NoViewEnum noView;
}
private static final String ENUM_JSON = a2q("{'a':'1','b':'2'}");
@Test
public void testEnumCreatorParamWithView() throws Exception
{
for (ObjectMapper mapper : new ObjectMapper[] { MAPPER, NO_DEFAULT_INCLUSION_MAPPER }) {
assertEquals(ParamViewEnum.A_ONLY, mapper.readerWithView(View1.class)
.forType(ParamViewEnum.class).readValue(ENUM_JSON));
assertEquals(ParamViewEnum.B_ONLY, mapper.readerWithView(View2.class)
.forType(ParamViewEnum.class).readValue(ENUM_JSON));
// and without active view, all parameters included
assertEquals(ParamViewEnum.BOTH, mapper.readValue(ENUM_JSON, ParamViewEnum.class));
}
}
@Test
public void testEnumCreatorParamWithTypeView() throws Exception
{
assertEquals(TypeViewEnum.A_ONLY, MAPPER.readerWithView(View1.class)
.forType(TypeViewEnum.class).readValue(ENUM_JSON));
assertEquals(TypeViewEnum.B_ONLY, MAPPER.readerWithView(View2.class)
.forType(TypeViewEnum.class).readValue(ENUM_JSON));
}
@Test
public void testEnumCreatorParamWithoutView() throws Exception
{
// By default un-annotated parameters are included regardless of active view
assertEquals(NoViewEnum.BOTH, MAPPER.readerWithView(View1.class)
.forType(NoViewEnum.class).readValue(ENUM_JSON));
// but if default inclusion disabled, excluded (same as with POJOs)
assertEquals(NoViewEnum.NONE, NO_DEFAULT_INCLUSION_MAPPER.readerWithView(View1.class)
.forType(NoViewEnum.class).readValue(ENUM_JSON));
// and without active view, all included
assertEquals(NoViewEnum.BOTH, NO_DEFAULT_INCLUSION_MAPPER.readValue(ENUM_JSON, NoViewEnum.class));
}
@Test
public void testEnumCreatorParamWithSuperView() throws Exception
{
// "b" has view `View`, which is visible in sub-views `View1` and `View2`;
// "a" has `View1` so is not visible in `View2`
assertEquals(SuperViewEnum.BOTH, MAPPER.readerWithView(View1.class)
.forType(SuperViewEnum.class).readValue(ENUM_JSON));
assertEquals(SuperViewEnum.B_ONLY, MAPPER.readerWithView(View2.class)
.forType(SuperViewEnum.class).readValue(ENUM_JSON));
}
@Test
public void testEnumCreatorParamViewViaMixIn() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.addMixIn(MixInViewEnum.class, MixInViewEnumMixIn.class)
.build();
assertEquals(MixInViewEnum.A_ONLY, mapper.readerWithView(View1.class)
.forType(MixInViewEnum.class).readValue(ENUM_JSON));
assertEquals(MixInViewEnum.B_ONLY, mapper.readerWithView(View2.class)
.forType(MixInViewEnum.class).readValue(ENUM_JSON));
}
// Hidden parameter must be skipped without trying to deserialize (or failing on) its value
@Test
public void testEnumCreatorHiddenParamWithStructuredValue() throws Exception
{
assertEquals(ParamViewEnum.A_ONLY, MAPPER.readerWithView(View1.class)
.forType(ParamViewEnum.class)
.readValue(a2q("{'b':{'x':[1,{'y':2}]},'a':'1'}")));
}
// Enum nested in POJO: the usual real-world case
@Test
public void testEnumCreatorNestedInPojo() throws Exception
{
final String json = a2q("{'param':{'a':'1','b':'2'},'noView':{'a':'1','b':'2'}}");
EnumHolder result = MAPPER.readerWithView(View1.class)
.forType(EnumHolder.class).readValue(json);
assertEquals(ParamViewEnum.A_ONLY, result.param);
assertEquals(NoViewEnum.BOTH, result.noView);
// With default inclusion disabled, un-annotated Enum parameters are hidden too
result = NO_DEFAULT_INCLUSION_MAPPER.readerWithView(View1.class)
.forType(EnumHolder.class).readValue(json);
assertEquals(ParamViewEnum.A_ONLY, result.param);
assertEquals(NoViewEnum.NONE, result.noView);
}
// Behavior pinned (same as for POJOs): a hidden parameter is treated as absent
// from input, even if it is `required` and actually present in the JSON
@Test
public void testEnumCreatorRequiredParamHiddenByView() throws Exception
{
// visible: fine
assertEquals(RequiredViewEnum.A_ONLY, MAPPER.readerWithView(View1.class)
.forType(RequiredViewEnum.class).readValue(ENUM_JSON));
assertEquals(RequiredViewEnum.BOTH, MAPPER.readValue(ENUM_JSON, RequiredViewEnum.class));
// hidden: reported as missing although present in input
Exception e = assertThrows(MismatchedInputException.class, () ->
MAPPER.readerWithView(View2.class)
.forType(RequiredViewEnum.class).readValue(ENUM_JSON));
assertTrue(e.getMessage().contains("Missing required creator property 'a'"),
() -> "Unexpected message: " + e.getMessage());
}
// Likewise for `FAIL_ON_MISSING_CREATOR_PROPERTIES`
@Test
public void testEnumCreatorParamHiddenByViewWithFailOnMissing() throws Exception
{
// no active view: all visible, fine
assertEquals(ParamViewEnum.BOTH,
FAIL_ON_MISSING_MAPPER.readValue(ENUM_JSON, ParamViewEnum.class));
// "b" hidden in View1: reported as missing although present in input
Exception e = assertThrows(MismatchedInputException.class, () ->
FAIL_ON_MISSING_MAPPER.readerWithView(View1.class)
.forType(ParamViewEnum.class).readValue(ENUM_JSON));
assertTrue(e.getMessage().contains("Missing creator property 'b'"),
() -> "Unexpected message: " + e.getMessage());
}
// Same for properties-based Creators of Map and EnumMap sub-types
@SuppressWarnings("serial")
static class ViewMap extends HashMap<String, String> {
public final int seen;
@JsonCreator
public ViewMap(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View2.class) String b) {
seen = seen(a, b);
}
}
enum MapKey { X, Y }
@SuppressWarnings("serial")
static class ViewEnumMap extends EnumMap<MapKey, String> {
public final int seen;
@JsonCreator
public ViewEnumMap(@JsonProperty("a") @JsonView(View1.class) String a,
@JsonProperty("b") @JsonView(View2.class) String b) {
super(MapKey.class);
seen = seen(a, b);
}
}
@Test
public void testMapCreatorParamWithView() throws Exception
{
assertEquals(1, MAPPER.readerWithView(View1.class)
.forType(ViewMap.class).<ViewMap>readValue(ENUM_JSON).seen);
assertEquals(2, MAPPER.readerWithView(View2.class)
.forType(ViewMap.class).<ViewMap>readValue(ENUM_JSON).seen);
assertEquals(3, MAPPER.readValue(ENUM_JSON, ViewMap.class).seen);
}
@Test
public void testEnumMapCreatorParamWithView() throws Exception
{
assertEquals(1, MAPPER.readerWithView(View1.class)
.forType(ViewEnumMap.class).<ViewEnumMap>readValue(ENUM_JSON).seen);
assertEquals(2, MAPPER.readerWithView(View2.class)
.forType(ViewEnumMap.class).<ViewEnumMap>readValue(ENUM_JSON).seen);
assertEquals(3, MAPPER.readValue(ENUM_JSON, ViewEnumMap.class).seen);
}
}