JOHNZON-382 Diagnose and guide users on invalid bean constructors
JOHNZON-376 Mapper API Bean constructor exceptions tests
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
index 435cf23..4243552 100644
--- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
@@ -450,8 +450,7 @@
public void arrayOfObjectFromObject() throws Exception {
assertMessage("{ \"arrayOfObject\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfObject' of type Color[] cannot be mapped to json object value: {\"red\":255,\"g" +
- "reen\":1...\nclass [Lorg.apache.johnzon.jsonb.DeserializationExceptionMessagesTest$Color; not instanti" +
- "able");
+ "reen\":1...\nColor[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -507,7 +506,7 @@
public void arrayOfStringFromObject() throws Exception {
assertMessage("{ \"arrayOfString\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfString' of type String[] cannot be mapped to json object value: {\"red\":255,\"" +
- "green\":1...\nclass [Ljava.lang.String; not instantiable");
+ "green\":1...\nString[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -558,7 +557,7 @@
public void arrayOfNumberFromObject() throws Exception {
assertMessage("{ \"arrayOfNumber\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfNumber' of type Number[] cannot be mapped to json object value: {\"red\":255,\"" +
- "green\":1...\nclass [Ljava.lang.Number; not instantiable");
+ "green\":1...\nNumber[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -588,7 +587,8 @@
public void arrayOfNumberFromArrayOfObject() throws Exception {
assertMessage("{ \"arrayOfNumber\" : [{\"red\": 255, \"green\": 165, \"blue\":0},{\"red\": 0, \"green\": 45, \"blue\":127}] }",
"Widget property 'arrayOfNumber' of type Number[] cannot be mapped to json array value: [{\"red\":255,\"" +
- "green\":...\njava.lang.InstantiationException");
+ "green\":...\nNumber cannot be constructed to deserialize json object value: {\"red\":255,\"green\":1...\nja" +
+ "va.lang.InstantiationException");
}
@Test
@@ -610,7 +610,7 @@
public void arrayOfBooleanFromObject() throws Exception {
assertMessage("{ \"arrayOfBoolean\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfBoolean' of type Boolean[] cannot be mapped to json object value: {\"red\":255" +
- ",\"green\":1...\nclass [Ljava.lang.Boolean; not instantiable");
+ ",\"green\":1...\nBoolean[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -661,7 +661,7 @@
public void arrayOfIntFromObject() throws Exception {
assertMessage("{ \"arrayOfInt\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfInt' of type int[] cannot be mapped to json object value: {\"red\":255,\"green\"" +
- ":1...\nclass [I not instantiable");
+ ":1...\nint[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -719,7 +719,7 @@
public void arrayOfByteFromObject() throws Exception {
assertMessage("{ \"arrayOfByte\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfByte' of type byte[] cannot be mapped to json object value: {\"red\":255,\"gree" +
- "n\":1...\nclass [B not instantiable");
+ "n\":1...\nbyte[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -770,7 +770,7 @@
public void arrayOfCharFromObject() throws Exception {
assertMessage("{ \"arrayOfChar\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfChar' of type char[] cannot be mapped to json object value: {\"red\":255,\"gree" +
- "n\":1...\nclass [C not instantiable");
+ "n\":1...\nchar[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -813,7 +813,7 @@
public void arrayOfShortFromObject() throws Exception {
assertMessage("{ \"arrayOfShort\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfShort' of type short[] cannot be mapped to json object value: {\"red\":255,\"gr" +
- "een\":1...\nclass [S not instantiable");
+ "een\":1...\nshort[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -864,7 +864,7 @@
public void arrayOfLongFromObject() throws Exception {
assertMessage("{ \"arrayOfLong\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfLong' of type long[] cannot be mapped to json object value: {\"red\":255,\"gree" +
- "n\":1...\nclass [J not instantiable");
+ "n\":1...\nlong[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -915,7 +915,7 @@
public void arrayOfFloatFromObject() throws Exception {
assertMessage("{ \"arrayOfFloat\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfFloat' of type float[] cannot be mapped to json object value: {\"red\":255,\"gr" +
- "een\":1...\nclass [F not instantiable");
+ "een\":1...\nfloat[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -966,7 +966,7 @@
public void arrayOfDoubleFromObject() throws Exception {
assertMessage("{ \"arrayOfDouble\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfDouble' of type double[] cannot be mapped to json object value: {\"red\":255,\"" +
- "green\":1...\nclass [D not instantiable");
+ "green\":1...\ndouble[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
}
@Test
@@ -1017,7 +1017,8 @@
public void arrayOfBooleanPrimitiveFromObject() throws Exception {
assertMessage("{ \"arrayOfBooleanPrimitive\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
"Widget property 'arrayOfBooleanPrimitive' of type boolean[] cannot be mapped to json object value: {" +
- "\"red\":255,\"green\":1...\nclass [Z not instantiable");
+ "\"red\":255,\"green\":1...\nboolean[] array not a suitable datatype for json object value: {\"red\":255,\"gr" +
+ "een\":1...");
}
@Test
@@ -1213,7 +1214,8 @@
public void listOfNumberFromArrayOfObject() throws Exception {
assertMessage("{ \"listOfNumber\" : [{\"red\": 255, \"green\": 165, \"blue\":0},{\"red\": 0, \"green\": 45, \"blue\":127}] }",
"Widget property 'listOfNumber' of type List<Number> cannot be mapped to json array value: [{\"red\":25" +
- "5,\"green\":...\njava.lang.InstantiationException");
+ "5,\"green\":...\nNumber cannot be constructed to deserialize json object value: {\"red\":255,\"green\":1..." +
+ "\njava.lang.InstantiationException");
}
@Test
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
index 209375c..36f3a28 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
@@ -365,7 +365,7 @@
*/
if (classMapping.factory == null) {
- throw new MapperException(classMapping.clazz + " not instantiable");
+ throw new MissingFactoryException(classMapping.clazz, object, config.getSnippet().of(object));
}
if (config.isFailOnUnknown()) {
@@ -377,11 +377,20 @@
}
Object t;
- if (classMapping.factory.getParameterTypes() == null || classMapping.factory.getParameterTypes().length == 0) {
- t = classMapping.factory.create(null);
- } else {
- t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
+ try {
+ if (classMapping.factory.getParameterTypes() == null || classMapping.factory.getParameterTypes().length == 0) {
+ t = classMapping.factory.create(null);
+ } else {
+ t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
+ }
+ } catch (Exception e) {
+ final String message = String.format("%s cannot be constructed to deserialize %s: %s%n%s",
+ ExceptionMessages.simpleName(type), ExceptionMessages.description(object),
+ config.getSnippet().of(object), e.getMessage()
+ );
+ throw new MapperException(message, e);
}
+
// store the new object under it's jsonPointer in case it gets referenced later
if (jsonPointers == null) {
if (classMapping.deduplicateObjects || config.isDeduplicateObjects()) {
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java
new file mode 100644
index 0000000..0f1ccc8
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.mapper;
+
+import javax.json.JsonObject;
+import java.lang.reflect.Constructor;
+import java.util.stream.Stream;
+
+
+public class MissingFactoryException extends MapperException {
+ public MissingFactoryException(final Class<?> clazz, final JsonObject object, final String json) {
+ super(message(clazz, object, json));
+ }
+
+ private static String message(final Class<?> clazz, final JsonObject object, final String json) {
+ if (clazz.isArray()) {
+ return String.format("%s array not a suitable datatype for %s: %s",
+ clazz.getSimpleName(),
+ ExceptionMessages.description(object),
+ json);
+ }
+
+ if (clazz.isInterface()) {
+ return String.format("%s is an interface and requires an adapter or factory. Cannot deserialize %s: %s%n%s not instantiable",
+ clazz.getSimpleName(),
+ ExceptionMessages.description(object),
+ json,
+ clazz);
+ }
+
+ String message = String.format("%s has no suitable constructor or factory. Cannot deserialize %s: %s",
+ clazz.getSimpleName(),
+ ExceptionMessages.description(object),
+ json);
+
+ final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+ final long constructorsWithParameters = Stream.of(constructors)
+ .filter(constructor -> constructor.getParameterTypes().length > 0)
+ .count();
+
+ // Was a constructor with parameters our only option? If so, help people
+ // learn how to properly use constructors with parameters
+ if (constructorsWithParameters > 0 && constructors.length == constructorsWithParameters) {
+ message += "\nUse Johnzon @ConstructorProperties or @JsonbCreator if constructor arguments are needed";
+ }
+
+ // Add full class name as final detail
+ message += "\n" + clazz + " not instantiable";
+ return message;
+ }
+}
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
index abaf530..7ada91f 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
@@ -17,6 +17,7 @@
package org.apache.johnzon.mapper;
import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Type;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -54,6 +55,14 @@
return this;
}
+ /**
+ * Useful for debugging tests
+ */
+ public ExceptionAsserts printStackTrace() {
+ throwable.printStackTrace();
+ return this;
+ }
+
public Throwable getThrowable() {
return throwable;
}
@@ -72,7 +81,7 @@
}
}
- public static ExceptionAsserts fromMapperReadObject(final String json, final Class<?> clazz) {
+ public static ExceptionAsserts fromMapperReadObject(final String json, final Type clazz) {
return from(() -> {
try (final Mapper mapper = new MapperBuilder().setSnippetMaxLength(20).build()) {
mapper.readObject(json, clazz);
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java
new file mode 100644
index 0000000..2615fe0
--- /dev/null
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.johnzon.mapper;
+
+import org.junit.Test;
+
+import java.beans.ConstructorProperties;
+import java.lang.reflect.Type;
+
+public class MapperBeanConstructorExceptionsTest {
+
+ private static final RuntimeException USER_EXCEPTION = new RuntimeException("I am user, hear me roar");
+
+ @Test
+ public void constructor() {
+ ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Circle.class)
+ .assertInstanceOf(MapperException.class)
+ .assertCauseChain(USER_EXCEPTION)
+ .assertMessage("Circle cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+ "java.lang.RuntimeException: I am user, hear me roar");
+ }
+
+ @Test
+ public void constructorParametersWithNoAnnotations() {
+ ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Square.class)
+ .assertInstanceOf(MapperException.class)
+ .assertMessage("Square has no suitable constructor or factory. Cannot deserialize json object value: {\"string\":\"Supercali...\n" +
+ "Use Johnzon @ConstructorProperties or @JsonbCreator if constructor arguments are needed\n" +
+ "class org.apache.johnzon.mapper.MapperBeanConstructorExceptionsTest$Square not instantiable");
+ }
+
+ @Test
+ public void constructorWithGenerics() {
+
+ final Type type = new Oval<String>(true) {
+ }.getClass().getGenericSuperclass();
+
+ ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", type)
+ .assertInstanceOf(MapperException.class)
+ .assertCauseChain(USER_EXCEPTION)
+ .assertMessage("Oval<String> cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+ "java.lang.RuntimeException: I am user, hear me roar");
+ }
+
+ @Test
+ public void constructorProperties() {
+ ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Triangle.class)
+ .assertInstanceOf(MapperException.class)
+ .assertCauseChain(USER_EXCEPTION)
+ .assertMessage("Triangle cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+ "java.lang.RuntimeException: I am user, hear me roar");
+ }
+
+ @Test
+ public void noConstructors() {
+ ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Sphere.class)
+ .assertInstanceOf(MapperException.class)
+ .assertMessage("Sphere is an interface and requires an adapter or factory. Cannot deserialize json object value: {\"string\":\"Supercali...\n" +
+ "interface org.apache.johnzon.mapper.MapperBeanConstructorExceptionsTest$Sphere not instantiable");
+ }
+
+ public static class Circle {
+ private String string;
+
+ public Circle() {
+ throw USER_EXCEPTION;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(final String string) {
+ this.string = string;
+ }
+ }
+
+ public static class Square {
+ private String string;
+
+ public Square(final String string) {
+ throw USER_EXCEPTION;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(final String string) {
+ this.string = string;
+ }
+ }
+
+ public static class Oval<T> {
+ private String s;
+
+ public Oval() {
+ throw USER_EXCEPTION;
+ }
+
+ public Oval(final boolean ignored) {
+ }
+ }
+
+ public static class Triangle {
+ private String string;
+
+ @ConstructorProperties("string")
+ public Triangle(final String string) {
+ throw USER_EXCEPTION;
+ }
+ }
+
+ public interface Sphere {
+ }
+
+
+}