ThrowableViewExplicit6190Test.java
package tools.jackson.databind.views;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests verifying that explicit {@code @JsonView} annotations placed on standard
* {@link Throwable} properties (such as "stackTrace" or "cause") are honored during
* deserialization (see [databind#6190]).
*/
public class ThrowableViewExplicit6190Test extends DatabindTestUtil
{
static class Public {}
static class Internal {}
// [databind#6190]: "message" and "suppressed" normally have no setter, so they are
// never bound as regular properties -- an explicit `@JsonView` on them must still
// be honored by the "unknown name" branches of the read loop
@SuppressWarnings("serial")
static class ExplicitMessageException extends RuntimeException {
public ExplicitMessageException() { super(); }
public ExplicitMessageException(String msg) { super(msg); }
@Override @JsonView(Internal.class)
public String getMessage() { return super.getMessage(); }
}
@SuppressWarnings("serial")
static class SuppressedException extends RuntimeException {
public SuppressedException() { super(); }
public SuppressedException(String msg) { super(msg); }
}
// `Throwable.getSuppressed()` is `final`, so a mix-in is the way to annotate it
static abstract class SuppressedViewMixIn {
@JsonView(Internal.class)
public abstract Throwable[] getSuppressed();
}
// Degenerate but legal: `@JsonView` listing no classes. Places its properties in no
// view at all -- and must do so for the standard `Throwable` ones as well, since the
// exemption is for a MISSING annotation, not an empty one
@JsonView({})
@SuppressWarnings("serial")
static class EmptyClassViewException extends RuntimeException {
public String custom;
public EmptyClassViewException() { super(); }
}
// No view annotation anywhere: the case the exemption exists for
@SuppressWarnings("serial")
static class NoViewException extends RuntimeException {
public NoViewException() { super(); }
}
// [databind#6190]: a class-level `@JsonView` is explicit intent too -- it covers
// every property of the class, including the standard `Throwable` ones
@JsonView(Internal.class)
@SuppressWarnings("serial")
static class ClassViewException extends RuntimeException {
public ClassViewException() { super(); }
public ClassViewException(String msg) { super(msg); }
}
// Same, but bound through a property-based Creator rather than setters
@JsonView(Internal.class)
@SuppressWarnings("serial")
static class ClassViewCreatorException extends RuntimeException {
@JsonCreator
public ClassViewCreatorException(@JsonProperty("message") String msg,
@JsonProperty("cause") Throwable cause) {
super(msg, cause);
}
}
// [databind#6190]: standard `Throwable` properties carrying explicit `@JsonView`
// must honor that view during deserialization
@SuppressWarnings("serial")
static class ExplicitStackTraceException extends RuntimeException {
public ExplicitStackTraceException() { super(); }
@Override @JsonView(Internal.class)
public void setStackTrace(StackTraceElement[] st) { super.setStackTrace(st); }
@Override @JsonView(Internal.class)
public StackTraceElement[] getStackTrace() { return super.getStackTrace(); }
}
@SuppressWarnings("serial")
static class ExplicitCauseException extends RuntimeException {
public ExplicitCauseException() { super(); }
@Override @JsonView(Internal.class)
public synchronized Throwable initCause(Throwable cause) { return super.initCause(cause); }
@Override @JsonView(Internal.class)
public Throwable getCause() { return super.getCause(); }
}
@SuppressWarnings("serial")
static class ExplicitGetCauseOnlyException extends RuntimeException {
public ExplicitGetCauseOnlyException() { super(); }
// Only annotate getCause, leaving initCause unannotated
@Override @JsonView(Internal.class)
public Throwable getCause() { return super.getCause(); }
}
private final ObjectMapper MAPPER = newJsonMapper();
private final ObjectMapper FAIL_ON_UNEXPECTED_MAPPER = jsonMapperBuilder()
.enable(DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES).build();
// [databind#6190]: explicit @JsonView on standard Throwable properties must be honored
@Test
public void explicitViewOnStackTraceRespected() throws Exception {
final String json = """
{
"stackTrace" : [ {
"className" : "some.Class", "methodName" : "m",
"fileName" : "C.java", "lineNumber" : 42
} ]
}
""";
// Under Public view, stackTrace restricted to Internal should NOT be populated from input
ExplicitStackTraceException exPublic = MAPPER.readerWithView(Public.class)
.forType(ExplicitStackTraceException.class)
.readValue(json);
assertTrue(_isFillInTrace(exPublic.getStackTrace()),
"stackTrace restricted to Internal must not be populated under Public view");
// Under Internal view, stackTrace SHOULD be populated from input (1 frame from input)
ExplicitStackTraceException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ExplicitStackTraceException.class)
.readValue(json);
assertEquals(1, exInternal.getStackTrace().length,
"stackTrace restricted to Internal should be set from input under Internal view");
assertFalse(_isFillInTrace(exInternal.getStackTrace()),
"trace read from input must not look like the JVM fill-in one");
// Under no view (full access), stackTrace and its element details SHOULD be populated
ExplicitStackTraceException exNoView = MAPPER.readerFor(ExplicitStackTraceException.class)
.readValue(json);
assertEquals(1, exNoView.getStackTrace().length);
assertEquals("some.Class", exNoView.getStackTrace()[0].getClassName());
}
// [databind#6190]: explicit @JsonView on cause must be honored
@Test
public void explicitViewOnCauseRespected() throws Exception {
final String json = """
{
"cause" : { "message" : "secret root cause" }
}
""";
ExplicitCauseException exPublic = MAPPER.readerWithView(Public.class)
.forType(ExplicitCauseException.class)
.readValue(json);
assertNull(exPublic.getCause(),
"cause restricted to Internal must not be populated under Public view");
ExplicitCauseException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ExplicitCauseException.class)
.readValue(json);
assertNotNull(exInternal.getCause());
assertEquals("secret root cause", exInternal.getCause().getMessage());
}
// [databind#6190]: explicit @JsonView on getter-only cause must also be honored
@Test
public void explicitViewOnGetCauseOnlyRespected() throws Exception {
final String json = """
{
"cause" : { "message" : "secret root cause" }
}
""";
ExplicitGetCauseOnlyException exPublic = MAPPER.readerWithView(Public.class)
.forType(ExplicitGetCauseOnlyException.class)
.readValue(json);
assertNull(exPublic.getCause(),
"cause with @JsonView on getCause only must not be populated under Public view");
ExplicitGetCauseOnlyException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ExplicitGetCauseOnlyException.class)
.readValue(json);
assertNotNull(exInternal.getCause());
assertEquals("secret root cause", exInternal.getCause().getMessage());
}
// [databind#6190]: with FAIL_ON_UNEXPECTED_VIEW_PROPERTIES, an explicit view on a
// standard property outside the active view must trigger an unexpected-view error
@Test
public void explicitViewOnStandardPropsFailsOnUnexpectedView() throws Exception {
final String json = """
{
"stackTrace" : [ {
"className" : "some.Class", "methodName" : "m",
"fileName" : "C.java", "lineNumber" : 42
} ]
}
""";
ObjectReader r = FAIL_ON_UNEXPECTED_MAPPER.readerWithView(Public.class)
.forType(ExplicitStackTraceException.class);
try {
r.readValue(json);
fail("should not pass, but fail with exception on unexpected view");
} catch (MismatchedInputException e) {
verifyException(e, "Input mismatch while deserializing");
verifyException(e, "Property 'stackTrace' is not part of current active view");
}
}
// [databind#6190]: explicit view must work with PropertyNamingStrategy
@Test
public void explicitViewUnderSnakeCaseNamingStrategy() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.build();
final String json = """
{
"stack_trace" : [ {
"class_name" : "some.Class", "method_name" : "m",
"file_name" : "C.java", "line_number" : 42
} ]
}
""";
ExplicitStackTraceException exPublic = mapper.readerWithView(Public.class)
.forType(ExplicitStackTraceException.class)
.readValue(json);
assertTrue(_isFillInTrace(exPublic.getStackTrace()),
"stack_trace restricted to Internal must not be populated under Public view");
ExplicitStackTraceException exInternal = mapper.readerWithView(Internal.class)
.forType(ExplicitStackTraceException.class)
.readValue(json);
assertEquals(1, exInternal.getStackTrace().length);
assertFalse(_isFillInTrace(exInternal.getStackTrace()),
"trace read from input must not look like the JVM fill-in one");
}
// [databind#6190]: the standard-property exemption must not discard a class-level
// `@JsonView`; it exists for properties defaulted out of every view, not for ones
// the user explicitly placed in a view
@Test
public void classLevelViewRespectedOnStandardProps() throws Exception {
final String json = """
{"message":"the msg","cause":{"message":"root"}}""";
// Whole class is Internal-only, so under Public view "cause" is not set
ClassViewException exPublic = MAPPER.readerWithView(Public.class)
.forType(ClassViewException.class).readValue(json);
assertNull(exPublic.getCause(),
"class-level @JsonView(Internal) must exclude 'cause' under Public view");
// ...but under Internal view it is
ClassViewException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ClassViewException.class).readValue(json);
assertNotNull(exInternal.getCause());
assertEquals("root", exInternal.getCause().getMessage());
}
// ...and the Creator-bound path must agree with the setter-bound one above
@Test
public void classLevelViewRespectedViaCreator() throws Exception {
final String json = """
{"message":"the msg","cause":{"message":"root"}}""";
ClassViewCreatorException exPublic = MAPPER.readerWithView(Public.class)
.forType(ClassViewCreatorException.class).readValue(json);
assertNull(exPublic.getCause(),
"class-level @JsonView(Internal) must exclude Creator-bound 'cause' under Public view");
ClassViewCreatorException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ClassViewCreatorException.class).readValue(json);
assertNotNull(exInternal.getCause());
assertEquals("root", exInternal.getCause().getMessage());
}
// [databind#6190]: explicit view on the non-settable "message"
@Test
public void explicitViewOnMessageRespected() throws Exception {
final String json = """
{"message":"the msg"}""";
ExplicitMessageException exPublic = MAPPER.readerWithView(Public.class)
.forType(ExplicitMessageException.class).readValue(json);
assertNull(exPublic.getMessage(),
"'message' restricted to Internal must not be set under Public view");
ExplicitMessageException exInternal = MAPPER.readerWithView(Internal.class)
.forType(ExplicitMessageException.class).readValue(json);
assertEquals("the msg", exInternal.getMessage());
// ...and with no active view at all it is set as before
ExplicitMessageException exNoView = MAPPER.readerFor(ExplicitMessageException.class)
.readValue(json);
assertEquals("the msg", exNoView.getMessage());
}
// ...same for the non-settable "suppressed"
@Test
public void explicitViewOnSuppressedRespected() throws Exception {
final String json = """
{"message":"the msg","suppressed":[{"message":"supp one"}]}""";
ObjectMapper mapper = jsonMapperBuilder()
.addMixIn(SuppressedException.class, SuppressedViewMixIn.class)
.build();
SuppressedException exPublic = mapper.readerWithView(Public.class)
.forType(SuppressedException.class).readValue(json);
assertEquals(0, exPublic.getSuppressed().length,
"'suppressed' restricted to Internal must not be set under Public view");
// "message" carries no view of its own, so it is still applied
assertEquals("the msg", exPublic.getMessage());
SuppressedException exInternal = mapper.readerWithView(Internal.class)
.forType(SuppressedException.class).readValue(json);
assertEquals(1, exInternal.getSuppressed().length);
assertEquals("supp one", exInternal.getSuppressed()[0].getMessage());
}
/**
* Tells whether given trace is the one the JVM filled in at construction, rather than
* one read from input. Checking for a frame belonging to this test class is
* deterministic, where checking the frame count is not (the JVM makes no promise about
* how deep a trace it fills in, so a trace of exactly one frame is possible).
*<p>
* NOTE: frame <i>content</i> cannot be used as the discriminator instead: under an
* active view the nested {@link StackTraceElement}'s own properties are themselves
* view-filtered, so a trace read from input comes back with blank class names.
*/
private boolean _isFillInTrace(StackTraceElement[] trace) {
for (StackTraceElement frame : trace) {
if (getClass().getName().equals(frame.getClassName())) {
return true;
}
}
return false;
}
// [databind#6190]: a view-excluded "message" must behave like a view-excluded bound
// property -- i.e. also honor FAIL_ON_UNEXPECTED_VIEW_PROPERTIES, not just be skipped
@Test
public void explicitViewOnMessageFailsOnUnexpectedView() throws Exception {
ObjectReader r = FAIL_ON_UNEXPECTED_MAPPER.readerWithView(Public.class)
.forType(ExplicitMessageException.class);
try {
r.readValue("""
{"message":"the msg"}""");
fail("should not pass, but fail with exception on unexpected view property");
} catch (MismatchedInputException e) {
verifyException(e, "Input mismatch while deserializing");
verifyException(e, "Property 'message' is not part of current active view");
}
}
// ...and the view check must not depend on some *other* property having views:
// `_needViewProcesing` is derived from settable properties only, and "message" is
// not one, so with DEFAULT_VIEW_INCLUSION enabled there may be none at all
@Test
public void explicitViewOnMessageRespectedWithDefaultViewInclusion() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.build();
ExplicitMessageException exPublic = mapper.readerWithView(Public.class)
.forType(ExplicitMessageException.class)
.readValue("""
{"message":"the msg"}""");
assertNull(exPublic.getMessage(),
"'message' restricted to Internal must not be set under Public view");
ExplicitMessageException exInternal = mapper.readerWithView(Internal.class)
.forType(ExplicitMessageException.class)
.readValue("""
{"message":"the msg"}""");
assertEquals("the msg", exInternal.getMessage());
}
// [databind#6190]: `@JsonView({})` on the class must treat the standard `Throwable`
// properties exactly as it treats regular ones -- excluded from every view. Note
// `BeanDescription.findDefaultViews()` cannot tell this apart from "no annotation"
// (both are an empty array), which is why the views are read off the introspector.
@Test
public void emptyClassLevelViewExcludesStandardProps() throws Exception {
final String json = """
{"custom":"c","cause":{"message":"root"}}""";
EmptyClassViewException ex = MAPPER.readerWithView(Public.class)
.forType(EmptyClassViewException.class).readValue(json);
// regular property: excluded from every view, as always
assertNull(ex.custom);
// ...and the standard property must agree, rather than being exempted
assertNull(ex.getCause(),
"@JsonView({}) on class must exclude 'cause' too, not exempt it");
}
// Control: with NO annotation the exemption stays intact -- that is the case
// [databind#6174] added it for
@Test
public void missingViewAnnotationKeepsExemption() throws Exception {
final String json = """
{"cause":{"message":"root"}}""";
NoViewException ex = MAPPER.readerWithView(Public.class)
.forType(NoViewException.class).readValue(json);
assertNotNull(ex.getCause(),
"standard props must stay included when no @JsonView is present at all");
}
}