Repository: Wikidata/Wikidata-Toolkit
Pull request #539 (2021-02-23 07:05:09)

Expose exceptions
Per #530, but only for three important classes: `JsonSerializer`, `WbGetEntitiesAction`, and `WbSearchEntitiesAction`. There are some spurious exception handlers in dumpfiles/rdf that I have not touched in this pull request.

Calling code within WDTK itself already assumes exceptions. There were no null checks anywhere, so this actually fixes a bug.

This is a breaking change but a very visible one, so there would be no silent failures in apps.

codecov[bot]: # [Codecov](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=h1) Report
> Merging [#539](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=desc) (dc0e35a) into [master](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/commit/3bc5d8f237b43c4080d67e65608133e11b1e90f4?el=desc) (3bc5d8f) will **decrease** coverage by `0.03%`.
> The diff coverage is `69.11%`.

[![Impacted file tree graph](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/graphs/tree.svg?width=650&height=150&src=pr&token=QtTNJdTAbO)](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #539      +/-   ##
============================================
- Coverage     81.04%   81.00%   -0.04%     
+ Complexity     2088     2085       -3     
============================================
  Files           149      149              
  Lines          7306     7286      -20     
  Branches        895      889       -6     
============================================
- Hits           5921     5902      -19     
+ Misses         1116     1115       -1     
  Partials        269      269              
```


| [Impacted Files](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...idata/wdtk/datamodel/helpers/JsonDeserializer.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kYXRhbW9kZWwvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kYXRhbW9kZWwvaGVscGVycy9Kc29uRGVzZXJpYWxpemVyLmphdmE=) | `95.23% <ø> (ø)` | `6.00 <0.00> (ø)` | |
| [...idata/wdtk/wikibaseapi/WbSearchEntitiesAction.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay13aWtpYmFzZWFwaS9zcmMvbWFpbi9qYXZhL29yZy93aWtpZGF0YS93ZHRrL3dpa2liYXNlYXBpL1diU2VhcmNoRW50aXRpZXNBY3Rpb24uamF2YQ==) | `63.63% <63.63%> (-0.26%)` | `5.00 <5.00> (-1.00)` | |
| [...wikidata/wdtk/wikibaseapi/WbGetEntitiesAction.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay13aWtpYmFzZWFwaS9zcmMvbWFpbi9qYXZhL29yZy93aWtpZGF0YS93ZHRrL3dpa2liYXNlYXBpL1diR2V0RW50aXRpZXNBY3Rpb24uamF2YQ==) | `86.66% <74.19%> (-1.22%)` | `18.00 <0.00> (-1.00)` | |
| [...ikidata/wdtk/datamodel/helpers/JsonSerializer.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kYXRhbW9kZWwvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kYXRhbW9kZWwvaGVscGVycy9Kc29uU2VyaWFsaXplci5qYXZh) | `68.88% <75.00%> (-2.54%)` | `11.00 <3.00> (-1.00)` | |
| [.../wikidata/wdtk/dumpfiles/EntityTimerProcessor.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kdW1wZmlsZXMvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kdW1wZmlsZXMvRW50aXR5VGltZXJQcm9jZXNzb3IuamF2YQ==) | `72.09% <0.00%> (-1.25%)` | `11.00% <0.00%> (ø%)` | |
| [...kidata/wdtk/rdf/values/QuantityValueConverter.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1yZGYvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9yZGYvdmFsdWVzL1F1YW50aXR5VmFsdWVDb252ZXJ0ZXIuamF2YQ==) | `71.42% <0.00%> (-0.99%)` | `5.00% <0.00%> (ø%)` | |
| [...a/wdtk/datamodel/interfaces/StatementDocument.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kYXRhbW9kZWwvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kYXRhbW9kZWwvaW50ZXJmYWNlcy9TdGF0ZW1lbnREb2N1bWVudC5qYXZh) | `71.23% <0.00%> (-0.77%)` | `45.00% <0.00%> (ø%)` | |
| [...a/wdtk/datamodel/implementation/ValueSnakImpl.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kYXRhbW9kZWwvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kYXRhbW9kZWwvaW1wbGVtZW50YXRpb24vVmFsdWVTbmFrSW1wbC5qYXZh) | `85.71% <0.00%> (-0.40%)` | `16.00% <0.00%> (ø%)` | |
| [...idata/wdtk/dumpfiles/DumpProcessingController.java](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539/diff?src=pr&el=tree#diff-d2R0ay1kdW1wZmlsZXMvc3JjL21haW4vamF2YS9vcmcvd2lraWRhdGEvd2R0ay9kdW1wZmlsZXMvRHVtcFByb2Nlc3NpbmdDb250cm9sbGVyLmphdmE=) | `65.60% <0.00%> (-0.22%)` | `32.00% <0.00%> (ø%)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=footer). Last update [3bc5d8f...dc0e35a](https://codecov.io/gh/Wikidata/Wikidata-Toolkit/pull/539?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

robertvazan: I am sorry for the large diff. As I have said in #535, there are no coding standards. Most code seems to use tabs. Eclipse doesn't work so well with mixed indentation. I have therefore converted spaces in affected code to tabs. Some code has indentation changes, because try/catch was removed. GitHub fortunately allows you to [hide whitespace changes](https://github.com/Wikidata/Wikidata-Toolkit/pull/539/files?w=1).

I am touching only three classes that are relevant in lexeme I/O. This is all a preparation for #403. Of these classes, `JsonSerializer` is really essential. The other two are changed for consistency.

I should clarify that this is not really a breaking change as long as WDTK correctly parses entities returned from Wikibase API. New exceptions are thrown only if parsing fails, which would imply a bug in WDTK. All code using `Wb*Action` classes is already handling `IOException`, so the new exceptions will be caught when Wikibase data model changes. Only direct `JsonSerializer` calls require application code changes, but that is only relevant when Wikibase data model changes. If that is a serious enough problem, I can create separate set of methods in `JsonSerializer` and deprecate the old ones.


Patch:
diff --git a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonDeserializer.java b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonDeserializer.java
index 4cab35b2..f0e3bb88 100644
--- a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonDeserializer.java
+++ b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonDeserializer.java
@@ -1,5 +1,3 @@
-package org.wikidata.wdtk.datamodel.helpers;
-
 /*-
  * #%L
  * Wikidata Toolkit Data Model
@@ -20,13 +18,24 @@ package org.wikidata.wdtk.datamodel.helpers;
  * #L%
  */
 
-import java.io.IOException;
+package org.wikidata.wdtk.datamodel.helpers;
 
-import com.fasterxml.jackson.databind.ObjectReader;
-import org.wikidata.wdtk.datamodel.implementation.*;
-import org.wikidata.wdtk.datamodel.interfaces.*;
+import org.wikidata.wdtk.datamodel.implementation.EntityDocumentImpl;
+import org.wikidata.wdtk.datamodel.implementation.EntityRedirectDocumentImpl;
+import org.wikidata.wdtk.datamodel.implementation.ItemDocumentImpl;
+import org.wikidata.wdtk.datamodel.implementation.LexemeDocumentImpl;
+import org.wikidata.wdtk.datamodel.implementation.MediaInfoDocumentImpl;
+import org.wikidata.wdtk.datamodel.implementation.PropertyDocumentImpl;
+import org.wikidata.wdtk.datamodel.interfaces.EntityDocument;
+import org.wikidata.wdtk.datamodel.interfaces.EntityRedirectDocument;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.LexemeDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MediaInfoDocument;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectReader;
 
 /**
  * Helper to deserialize datamodel objects from their
@@ -72,55 +81,55 @@ public class JsonDeserializer {
 	
 	/**
 	 * Deserializes a JSON string into an {@link ItemDocument}.
-	 * @throws IOException 
+	 * @throws JsonProcessingException 
 			if the JSON payload is invalid
 	 */
-	public ItemDocument deserializeItemDocument(String json) throws IOException {
+	public ItemDocument deserializeItemDocument(String json) throws JsonProcessingException {
 		return itemReader.readValue(json);
 	}
 	
 	/**
 	 * Deserializes a JSON string into a {@link PropertyDocument}.
-	 * @throws IOException 
+	 * @throws JsonProcessingException 
 			if the JSON payload is invalid
 	 */
-	public PropertyDocument deserializePropertyDocument(String json) throws IOException {
+	public PropertyDocument deserializePropertyDocument(String json) throws JsonProcessingException {
 		return propertyReader.readValue(json);
 	}
 
 	/**
 	 * Deserializes a JSON string into a {@link LexemeDocument}.
-	 * @throws IOException 
+	 * @throws JsonProcessingException 
 			if the JSON payload is invalid
 	 */
-	public LexemeDocument deserializeLexemeDocument(String json) throws IOException {
+	public LexemeDocument deserializeLexemeDocument(String json) throws JsonProcessingException {
 		return lexemeReader.readValue(json);
 	}
 	
 	/**
 	 * Deserializes a JSON string into a {@link MediaInfoDocument}.
-	 * @throws IOException 
+	 * @throws JsonProcessingException 
 			if the JSON payload is invalid
 	 */
-	public MediaInfoDocument deserializeMediaInfoDocument(String json) throws IOException {
+	public MediaInfoDocument deserializeMediaInfoDocument(String json) throws JsonProcessingException {
 		return mediaInfoReader.readValue(json);
 	}
 	
 	/**
 	 * Deserializes a JSON string into a {@link EntityDocument}.
-	 * @throws IOException 
+	 * @throws JsonProcessingException 
 			if the JSON payload is invalid
 	 */
-	public EntityDocument deserializeEntityDocument(String json) throws IOException {
+	public EntityDocument deserializeEntityDocument(String json) throws JsonProcessingException {
 		return entityDocumentReader.readValue(json);
 	}
 
 	/**
 	 * Deserializes a JSON string into a {@link EntityRedirectDocument}.
-	 * @throws IOException
+	 * @throws JsonProcessingException
 	if the JSON payload is invalid
 	 */
-	public EntityRedirectDocument deserializeEntityRedirectDocument(String json) throws IOException {
+	public EntityRedirectDocument deserializeEntityRedirectDocument(String json) throws JsonProcessingException {
 		return entityRedirectReader.readValue(json);
 	}
 }
diff --git a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializer.java b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializer.java
index 369905c2..977e0e78 100644
--- a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializer.java
+++ b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializer.java
@@ -1,5 +1,3 @@
-package org.wikidata.wdtk.datamodel.helpers;
-
 /*
  * #%L
  * Wikidata Toolkit Data Model
@@ -20,6 +18,8 @@ package org.wikidata.wdtk.datamodel.helpers;
  * #L%
  */
 
+package org.wikidata.wdtk.datamodel.helpers;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
@@ -173,69 +173,54 @@ public class JsonSerializer implements EntityDocumentDumpProcessor {
 	}
 
 	/**
-	 * Serializes the given object in JSON and returns the resulting string. In
-	 * case of errors, null is returned.
+	 * Serializes the given object in JSON and returns the resulting string.
+	 * Throws if the serialization fails.
 	 *
 	 * @param itemDocument
 	 *            object to serialize
-	 * @return JSON serialization or null
+	 * @return JSON serialization
+	 * @throws JsonProcessingException if the object cannot be serialized
 	 */
-	public static String getJsonString(ItemDocument itemDocument) {
-		return jacksonObjectToString(itemDocument);
+	public static String getJsonString(ItemDocument itemDocument) throws JsonProcessingException {
+		return mapper.writeValueAsString(itemDocument);
 	}
 
 	/**
-	 * Serializes the given object in JSON and returns the resulting string. In
-	 * case of errors, null is returned.
+	 * Serializes the given object in JSON and returns the resulting string.
+	 * Throws if the serialization fails.
 	 *
 	 * @param propertyDocument
 	 *            object to serialize
-	 * @return JSON serialization or null
+	 * @return JSON serialization
+	 * @throws JsonProcessingException if the object cannot be serialized
 	 */
-	public static String getJsonString(PropertyDocument propertyDocument) {
-		return jacksonObjectToString(propertyDocument);
+	public static String getJsonString(PropertyDocument propertyDocument) throws JsonProcessingException {
+		return mapper.writeValueAsString(propertyDocument);
 	}
 
 	/**
-	 * Serializes the given object in JSON and returns the resulting string. In
-	 * case of errors, null is returned.
+	 * Serializes the given object in JSON and returns the resulting string.
+	 * Throws if the serialization fails.
 	 *
 	 * @param mediaInfoDocument
 	 *            object to serialize
-	 * @return JSON serialization or null
+	 * @return JSON serialization
+	 * @throws JsonProcessingException if the object cannot be serialized
 	 */
-	public static String getJsonString(MediaInfoDocument mediaInfoDocument) {
-		return jacksonObjectToString(mediaInfoDocument);
+	public static String getJsonString(MediaInfoDocument mediaInfoDocument) throws JsonProcessingException {
+		return mapper.writeValueAsString(mediaInfoDocument);
 	}
 
 	/**
-	 * Serializes the given object in JSON and returns the resulting string. In
-	 * case of errors, null is returned.
+	 * Serializes the given object in JSON and returns the resulting string.
+	 * Throws if the serialization fails.
 	 *
 	 * @param statement
 	 *            object to serialize
-	 * @return JSON serialization or null
-	 */
-	public static String getJsonString(Statement statement) {
-		return jacksonObjectToString(statement);
-	}
-
-	/**
-	 * Serializes the given object in JSON and returns the resulting string. In
-	 * case of errors, null is returned. In particular, this happens if the
-	 * object is not based on a Jackson-annotated class. An error is logged in
-	 * this case.
-	 *
-	 * @param object
-	 *            object to serialize
-	 * @return JSON serialization or null
+	 * @return JSON serialization
+	 * @throws JsonProcessingException if the object cannot be serialized
 	 */
-	protected static String jacksonObjectToString(Object object) {
-		try {
-			return mapper.writeValueAsString(object);
-		} catch (JsonProcessingException e) {
-			logger.error("Failed to serialize JSON data: " + e.toString());
-			return null;
-		}
+	public static String getJsonString(Statement statement) throws JsonProcessingException {
+		return mapper.writeValueAsString(statement);
 	}
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbGetEntitiesAction.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbGetEntitiesAction.java
index 30e08029..4378f416 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbGetEntitiesAction.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbGetEntitiesAction.java
@@ -1,5 +1,3 @@
-package org.wikidata.wdtk.wikibaseapi;
-
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -20,21 +18,31 @@ package org.wikidata.wdtk.wikibaseapi;
  * #L%
  */
 
+package org.wikidata.wdtk.wikibaseapi;
+
 import java.io.IOException;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
 
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.wikidata.wdtk.datamodel.helpers.Datamodel;
 import org.wikidata.wdtk.datamodel.helpers.DatamodelMapper;
 import org.wikidata.wdtk.datamodel.implementation.EntityDocumentImpl;
 import org.wikidata.wdtk.datamodel.implementation.EntityIdValueImpl;
-import org.wikidata.wdtk.datamodel.interfaces.*;
+import org.wikidata.wdtk.datamodel.interfaces.EntityDocument;
+import org.wikidata.wdtk.datamodel.interfaces.EntityIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MediaInfoDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MediaInfoIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.SiteLink;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
@@ -46,9 +54,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
  */
 public class WbGetEntitiesAction {
 
-	static final Logger logger = LoggerFactory
-			.getLogger(WbGetEntitiesAction.class);
-
 	/**
 	 * Connection to an Wikibase API.
 	 */
@@ -94,6 +99,8 @@ public class WbGetEntitiesAction {
 	 *             if the API returns an error
 	 * @throws IOException
 	 * 			   if we encounter network issues or HTTP 500 errors from Wikibase
+	 * @throws MalformedResponseException
+	 *             if one of the returned entities cannot be parsed
 	 */
 	public Map<String, EntityDocument> wbGetEntities(
 			WbGetEntitiesActionData properties)
@@ -112,8 +119,8 @@ public class WbGetEntitiesAction {
 	 * 500 for bots. This limit may also apply to the number of language codes
 	 * and sites used for filtering.
 	 * <p>
-	 * If errors occur (e.g., exceptions trying to access the Web API), then the
-	 * errors will be logged and an empty collection will be returned.
+	 * If an error occurs (e.g., exceptions trying to access the Web API),
+	 * the exception will be propagated to the caller.
 	 *
 	 * @param ids
 	 *            list of ids of entities for which data should be retrieved
@@ -150,6 +157,8 @@ public class WbGetEntitiesAction {
 	 *             if we encounter network errors, or HTTP 500 errors on Wikibase's side
 	 * @throws IllegalArgumentException
 	 *             if the given combination of parameters does not make sense
+	 * @throws MalformedResponseException
+	 *             if one of the returned entities cannot be parsed
 	 */
 	public Map<String, EntityDocument> wbGetEntities(String ids, String sites,
 			String titles, String props, String languages, String sitefilter)
@@ -190,56 +199,51 @@ public class WbGetEntitiesAction {
 
 		Map<String, EntityDocument> result = new HashMap<>();
 
-		try {
-			JsonNode root = this.connection.sendJsonRequest("POST", parameters);
-
-			JsonNode entities = root.path("entities");
-			Iterator<Entry<String,JsonNode>> entitiesIterator = entities.fields();
-			int i = 0;
-			while(entitiesIterator.hasNext()) {
-				Entry<String,JsonNode> entry = entitiesIterator.next();
-				JsonNode entityNode = entry.getValue();
-				if(!entityNode.has("missing")) {
-					try {
-						EntityDocument ed = mapper.reader()
-								.with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
-								.treeToValue(entityNode, EntityDocumentImpl.class);
-
-						if (titles == null) {
-							// We use the JSON key rather than the id of the value
-							// so that retrieving redirected entities works.
-							result.put(entry.getKey(), ed);
-						} else {
-							if (ed instanceof ItemDocument) {
-								SiteLink siteLink = ((ItemDocument) ed).getSiteLinks().get(sites);
-								if(siteLink != null) {
-									result.put(siteLink.getPageTitle(), ed);
-								}
-							} else if(ed instanceof MediaInfoDocument) {
-								result.put(entityNode.get("title").textValue(), ed);
+		JsonNode root = this.connection.sendJsonRequest("POST", parameters);
+
+		JsonNode entities = root.path("entities");
+		Iterator<Entry<String,JsonNode>> entitiesIterator = entities.fields();
+		int i = 0;
+		while(entitiesIterator.hasNext()) {
+			Entry<String,JsonNode> entry = entitiesIterator.next();
+			JsonNode entityNode = entry.getValue();
+			if(!entityNode.has("missing")) {
+				try {
+					EntityDocument ed = mapper.reader()
+							.with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+							.treeToValue(entityNode, EntityDocumentImpl.class);
+
+					if (titles == null) {
+						// We use the JSON key rather than the id of the value
+						// so that retrieving redirected entities works.
+						result.put(entry.getKey(), ed);
+					} else {
+						if (ed instanceof ItemDocument) {
+							SiteLink siteLink = ((ItemDocument) ed).getSiteLinks().get(sites);
+							if(siteLink != null) {
+								result.put(siteLink.getPageTitle(), ed);
 							}
+						} else if(ed instanceof MediaInfoDocument) {
+							result.put(entityNode.get("title").textValue(), ed);
 						}
-					} catch (JsonProcessingException e) {
-						logger.error("Error when reading JSON for entity "
-								+ entityNode.path("id").asText("UNKNOWN")
-								+ ": " + e.toString());
 					}
-				} else if(entityNode.has("id")) {
-					try {
-						EntityIdValue entityIdValue = EntityIdValueImpl.fromId(entityNode.get("id").asText(), siteIri);
-						if(entityIdValue instanceof MediaInfoIdValue) {
-							//TODO: bad hack, it would be much nicer if the API would return the page title
-							result.put(titlesList.get(i), Datamodel.makeMediaInfoDocument((MediaInfoIdValue) entityIdValue));
-						}
-					} catch (IllegalArgumentException e) {
-						logger.warn("Invalid entity id returned: " + entityNode.get("id").asText());
+				} catch (JsonProcessingException e) {
+					throw new MalformedResponseException(
+							"Error when reading JSON for entity " + entityNode.path("id").asText("UNKNOWN"), e);
+				}
+			} else if(entityNode.has("id")) {
+				try {
+					EntityIdValue entityIdValue = EntityIdValueImpl.fromId(entityNode.get("id").asText(), siteIri);
+					if(entityIdValue instanceof MediaInfoIdValue) {
+						//TODO: bad hack, it would be much nicer if the API would return the page title
+						result.put(titlesList.get(i), Datamodel.makeMediaInfoDocument((MediaInfoIdValue) entityIdValue));
 					}
+				} catch (IllegalArgumentException e) {
+					throw new MalformedResponseException(
+							"Invalid entity id returned: " + entityNode.get("id").asText(), e);
 				}
-				i++;
 			}
-		} catch (IOException e) {
-			logger.error("Could not retrive data: " + e.toString());
-			throw e;
+			i++;
 		}
 
 		return result;
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbSearchEntitiesAction.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbSearchEntitiesAction.java
index 04c0dbf8..bab63c08 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbSearchEntitiesAction.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbSearchEntitiesAction.java
@@ -1,5 +1,3 @@
-package org.wikidata.wdtk.wikibaseapi;
-
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -20,14 +18,14 @@ package org.wikidata.wdtk.wikibaseapi;
  * #L%
  */
 
+package org.wikidata.wdtk.wikibaseapi;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -42,132 +40,130 @@ import com.fasterxml.jackson.databind.ObjectMapper;
  */
 public class WbSearchEntitiesAction {
 
-    private static final Logger LOGGER = LoggerFactory
-            .getLogger(WbSearchEntitiesAction.class);
-
-    /**
-     * Connection to a Wikibase API.
-     */
-    private final ApiConnection connection;
-
-    /**
-     * Mapper object used for deserializing JSON data.
-     */
-    private final ObjectMapper mapper = new ObjectMapper();
-
-    /**
-     * Creates an object to fetch data from the given ApiConnection. The site
-     * URI is necessary since it is not contained in the data retrieved from the
-     * API.
-     *
-     * @param connection
-     *            {@link ApiConnection} Object to send the requests
-     * @param siteUri
-     *            the URI identifying the site that is accessed (usually the
-     *            prefix of entity URIs), e.g.,
-     *            "http://www.wikidata.org/entity/"
-     */
-    public WbSearchEntitiesAction(ApiConnection connection, String siteUri) {
-        this.connection = connection;
-    }
-
-    public List<WbSearchEntitiesResult> wbSearchEntities(WbGetEntitiesSearchData properties)
-            throws MediaWikiApiErrorException, IOException {
-        return wbSearchEntities(properties.search, properties.language,
-                properties.strictlanguage, properties.type, properties.limit, properties.offset);
-    }
-
-    /**
-     * Executes the API action "wbsearchentity" for the given parameters.
-     * Searches for entities using labels and aliases. Returns a label and
-     * description for the entity in the user language if possible. Returns
-     * details of the matched term. The matched term text is also present in the
-     * aliases key if different from the display label.
-     *
-     * <p>
-     * See the <a href=
-     * "https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentity"
-     * >online API documentation</a> for further information.
-     * <p>
-     *
-     * @param search
-     *            (required) search for this text
-     * @param language
-     *            (required) search in this language
-     * @param strictLanguage
-     *            (optional) whether to disable language fallback
-     * @param type
-     *            (optional) search for this type of entity
-     *            One of the following values: item, property
-     *            Default: item
-     * @param limit
-     *            (optional) maximal number of results
-     *            no more than 50 (500 for bots) allowed
-     *            Default: 7
-     * @param offset
-     *            (optional) offset where to continue a search
-     *            Default: 0
-     *            this parameter is called "continue" in the API (which is a Java keyword)
-     *
-     * @return list of matching entities retrieved via the API URL
-     * @throws MediaWikiApiErrorException
-     *             if the API returns an error
-     * @throws IllegalArgumentException
-     *             if the given combination of parameters does not make sense
-     */
-    public List<WbSearchEntitiesResult> wbSearchEntities(String search, String language,
-                                                         Boolean strictLanguage, String type, Long limit, Long offset)
-            throws MediaWikiApiErrorException, IOException {
-
-        Map<String, String> parameters = new HashMap<>();
-        parameters.put(ApiConnection.PARAM_ACTION, "wbsearchentities");
-
-        if (search != null) {
-            parameters.put("search", search);
-        } else {
-            throw new IllegalArgumentException(
-                    "Search parameter must be specified for this action.");
-        }
-
-        if (language != null) {
-            parameters.put("language", language);
-        } else {
-            throw new IllegalArgumentException(
-                    "Language parameter must be specified for this action.");
-        }
-        if (strictLanguage != null) {
-            parameters.put("strictlanguage", Boolean.toString(strictLanguage));
-        }
-
-        if (type != null) {
-            parameters.put("type", type);
-        }
-
-        if (limit != null) {
-            parameters.put("limit", Long.toString(limit));
-        }
-
-        if (offset != null) {
-            parameters.put("continue", Long.toString(offset));
-        }
-
-        List<WbSearchEntitiesResult> results = new ArrayList<>();
-
-        JsonNode root = this.connection.sendJsonRequest("POST", parameters);
-        JsonNode entities = root.path("search");
-        for (JsonNode entityNode : entities) {
-            try {
-                JacksonWbSearchEntitiesResult ed = mapper.treeToValue(entityNode,
-                        JacksonWbSearchEntitiesResult.class);
-                results.add(ed);
-            } catch (JsonProcessingException e) {
-                LOGGER.error("Error when reading JSON for entity "
-                        + entityNode.path("id").asText("UNKNOWN") + ": "
-                        + e.toString());
-            }
-        }
-
-        return results;
-    }
+	/**
+	 * Connection to a Wikibase API.
+	 */
+	private final ApiConnection connection;
+
+	/**
+	 * Mapper object used for deserializing JSON data.
+	 */
+	private final ObjectMapper mapper = new ObjectMapper();
+
+	/**
+	 * Creates an object to fetch data from the given ApiConnection. The site
+	 * URI is necessary since it is not contained in the data retrieved from the
+	 * API.
+	 *
+	 * @param connection
+	 *            {@link ApiConnection} Object to send the requests
+	 * @param siteUri
+	 *            the URI identifying the site that is accessed (usually the
+	 *            prefix of entity URIs), e.g.,
+	 *            "http://www.wikidata.org/entity/"
+	 */
+	public WbSearchEntitiesAction(ApiConnection connection, String siteUri) {
+		this.connection = connection;
+	}
+
+	public List<WbSearchEntitiesResult> wbSearchEntities(WbGetEntitiesSearchData properties)
+			throws MediaWikiApiErrorException, IOException {
+		return wbSearchEntities(properties.search, properties.language,
+				properties.strictlanguage, properties.type, properties.limit, properties.offset);
+	}
+
+	/**
+	 * Executes the API action "wbsearchentity" for the given parameters.
+	 * Searches for entities using labels and aliases. Returns a label and
+	 * description for the entity in the user language if possible. Returns
+	 * details of the matched term. The matched term text is also present in the
+	 * aliases key if different from the display label.
+	 *
+	 * <p>
+	 * See the <a href=
+	 * "https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentity"
+	 * >online API documentation</a> for further information.
+	 * <p>
+	 *
+	 * @param search
+	 *            (required) search for this text
+	 * @param language
+	 *            (required) search in this language
+	 * @param strictLanguage
+	 *            (optional) whether to disable language fallback
+	 * @param type
+	 *            (optional) search for this type of entity
+	 *            One of the following values: item, property
+	 *            Default: item
+	 * @param limit
+	 *            (optional) maximal number of results
+	 *            no more than 50 (500 for bots) allowed
+	 *            Default: 7
+	 * @param offset
+	 *            (optional) offset where to continue a search
+	 *            Default: 0
+	 *            this parameter is called "continue" in the API (which is a Java keyword)
+	 *
+	 * @return list of matching entities retrieved via the API URL
+	 * @throws MediaWikiApiErrorException
+	 *             if the API returns an error
+	 * @throws IllegalArgumentException
+	 *             if the given combination of parameters does not make sense
+	 * @throws MalformedResponseException
+	 *             if response JSON cannot be parsed
+	 */
+	public List<WbSearchEntitiesResult> wbSearchEntities(String search, String language,
+			Boolean strictLanguage, String type, Long limit, Long offset)
+					throws MediaWikiApiErrorException, IOException {
+
+		Map<String, String> parameters = new HashMap<>();
+		parameters.put(ApiConnection.PARAM_ACTION, "wbsearchentities");
+
+		if (search != null) {
+			parameters.put("search", search);
+		} else {
+			throw new IllegalArgumentException(
+					"Search parameter must be specified for this action.");
+		}
+
+		if (language != null) {
+			parameters.put("language", language);
+		} else {
+			throw new IllegalArgumentException(
+					"Language parameter must be specified for this action.");
+		}
+		if (strictLanguage != null) {
+			parameters.put("strictlanguage", Boolean.toString(strictLanguage));
+		}
+
+		if (type != null) {
+			parameters.put("type", type);
+		}
+
+		if (limit != null) {
+			parameters.put("limit", Long.toString(limit));
+		}
+
+		if (offset != null) {
+			parameters.put("continue", Long.toString(offset));
+		}
+
+		List<WbSearchEntitiesResult> results = new ArrayList<>();
+
+		JsonNode root = this.connection.sendJsonRequest("POST", parameters);
+		JsonNode entities = root.path("search");
+		for (JsonNode entityNode : entities) {
+			try {
+				JacksonWbSearchEntitiesResult ed = mapper.treeToValue(entityNode,
+						JacksonWbSearchEntitiesResult.class);
+				results.add(ed);
+			} catch (JsonProcessingException e) {
+				throw new MalformedResponseException(
+						"Error when reading JSON for entity " + entityNode.path("id").asText("UNKNOWN"), e);
+			}
+		}
+
+		return results;
+	}
 
 }


Test patch:
diff --git a/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializerTest.java b/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializerTest.java
index 3e64c35c..76a725ff 100644
--- a/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializerTest.java
+++ b/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/helpers/JsonSerializerTest.java
@@ -21,7 +21,7 @@
 package org.wikidata.wdtk.datamodel.helpers;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -29,6 +29,8 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 import org.wikidata.wdtk.datamodel.implementation.EntityDocumentImpl;
@@ -37,10 +39,14 @@ import org.wikidata.wdtk.datamodel.interfaces.DatatypeIdValue;
 import org.wikidata.wdtk.datamodel.interfaces.EntityDocument;
 import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
 import org.wikidata.wdtk.datamodel.interfaces.ItemIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.MonolingualTextValue;
 import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
+import org.wikidata.wdtk.datamodel.interfaces.SiteLink;
 import org.wikidata.wdtk.datamodel.interfaces.Statement;
+import org.wikidata.wdtk.datamodel.interfaces.StatementGroup;
 import org.wikidata.wdtk.datamodel.interfaces.StatementRank;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
@@ -97,7 +103,7 @@ public class JsonSerializerTest {
 	}
 
 	@Test
-	public void testItemDocumentToJson() {
+	public void testItemDocumentToJson() throws JsonProcessingException {
 		ItemDocument id = Datamodel.makeItemDocument(
 				Datamodel.makeWikidataItemIdValue("Q42"),
 				Collections.emptyList(),
@@ -110,7 +116,7 @@ public class JsonSerializerTest {
 	}
 
 	@Test
-	public void testPropertyDocumentToJson() {
+	public void testPropertyDocumentToJson() throws JsonProcessingException {
 		PropertyDocument pd = Datamodel.makePropertyDocument(
 				Datamodel.makeWikidataPropertyIdValue("P1"),
 				Collections. emptyList(),
@@ -123,7 +129,7 @@ public class JsonSerializerTest {
 	}
 
 	@Test
-	public void testStatementToJson() {
+	public void testStatementToJson() throws JsonProcessingException {
 		Statement s = Datamodel.makeStatement(ItemIdValue.NULL,
 				Datamodel.makeNoValueSnak(Datamodel.makeWikidataPropertyIdValue("P1")),
 				Collections.emptyList(), Collections.emptyList(),
@@ -134,14 +140,74 @@ public class JsonSerializerTest {
 
 	@Test
 	public void testJacksonObjectToJsonError() {
-		Object obj = new Object() {
-			@SuppressWarnings("unused")
-			public String getData() {
-				throw new RuntimeException("Test exception");
+		ItemDocument obj = new ItemDocument() {
+
+			@Override
+			public List<StatementGroup> getStatementGroups() {
+				return null;
+			}
+
+			@Override
+			public long getRevisionId() {
+				return 0;
+			}
+
+			@Override
+			public Map<String, MonolingualTextValue> getLabels() {
+				return null;
+			}
+
+			@Override
+			public Map<String, MonolingualTextValue> getDescriptions() {
+				return null;
+			}
+
+			@Override
+			public Map<String, List<MonolingualTextValue>> getAliases() {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withoutStatementIds(Set<String> statementIds) {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withStatement(Statement statement) {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withRevisionId(long newRevisionId) {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withLabel(MonolingualTextValue newLabel) {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withDescription(MonolingualTextValue newDescription) {
+				return null;
+			}
+
+			@Override
+			public ItemDocument withAliases(String language, List<MonolingualTextValue> aliases) {
+				return null;
+			}
+
+			@Override
+			public Map<String, SiteLink> getSiteLinks() {
+				return null;
+			}
+
+			@Override
+			public ItemIdValue getEntityId() {
+				return null;
 			}
 		};
 
-		String result = JsonSerializer.jacksonObjectToString(obj);
-		assertNull(result);
+		assertThrows(JsonProcessingException.class, () -> JsonSerializer.getJsonString(obj));
 	}
 }
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/StatementUpdateTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/StatementUpdateTest.java
index 3f9a56a5..8eb0d70b 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/StatementUpdateTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/StatementUpdateTest.java
@@ -42,6 +42,7 @@ import org.wikidata.wdtk.datamodel.interfaces.Reference;
 import org.wikidata.wdtk.datamodel.interfaces.Statement;
 import org.wikidata.wdtk.datamodel.interfaces.StatementRank;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
@@ -215,7 +216,7 @@ public class StatementUpdateTest {
 	}
 
 	@Test
-	public void testAddStatements() {
+	public void testAddStatements() throws JsonProcessingException {
 		// Inserting new P2 statements won't touch existing P1 statement
 		Statement s1 = StatementBuilder.forSubjectAndProperty(Q1, P1)
 				.withValue(Q1).withId("ID-s1").build();