diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java index 281b5620..ecaf07c2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java @@ -28,6 +28,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -62,11 +63,29 @@ public class JsonUtils ** *******************************************************************************/ public static String toJson(Object object) + { + return (toJson(object, null)); + } + + + + /******************************************************************************* + ** Serialize any object into a JSON String - with customizations on the Jackson + ** ObjectMapper. + ** + ** Internally using jackson - so jackson annotations apply! + ** + *******************************************************************************/ + public static String toJson(Object object, Consumer objectMapperCustomizer) { try { - ObjectMapper mapper = newObjectMapper(); - String jsonResult = mapper.writeValueAsString(object); + ObjectMapper mapper = newObjectMapper(); + if(objectMapperCustomizer != null) + { + objectMapperCustomizer.accept(mapper); + } + String jsonResult = mapper.writeValueAsString(object); return (jsonResult); } catch(JsonProcessingException e) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/JsonUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/JsonUtilsTest.java index de9121b3..f049ad44 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/JsonUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/JsonUtilsTest.java @@ -28,6 +28,7 @@ import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonInclude; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; @@ -38,6 +39,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -68,6 +70,24 @@ class JsonUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test_toJsonQRecordInputWithNullValues() + { + QRecord qRecord = getQRecord(); + String json = JsonUtils.toJson(qRecord, objectMapper -> + { + objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + }); + + assertThat(json).contains(""" + "values":{"foo":"Foo","bar":3.14159,"baz":null}"""); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -213,6 +233,7 @@ class JsonUtilsTest extends BaseTest qRecord.setValues(values); values.put("foo", "Foo"); values.put("bar", new BigDecimal("3.14159")); + values.put("baz", null); return qRecord; } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 5bcd5f02..98484dce 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; import com.kingsrook.qqq.api.model.APILog; @@ -297,7 +298,7 @@ public class QJavalinApiHandler } context.contentType(ContentType.APPLICATION_JSON); - context.result(JsonUtils.toJson(rs)); + context.result(toJson(rs)); } @@ -312,7 +313,7 @@ public class QJavalinApiHandler rs.put("currentVersion", apiInstanceMetaData.getCurrentVersion().toString()); context.contentType(ContentType.APPLICATION_JSON); - context.result(JsonUtils.toJson(rs)); + context.result(toJson(rs)); } @@ -655,7 +656,7 @@ public class QJavalinApiHandler Map outputRecord = ApiImplementation.get(apiInstanceMetaData, version, tableApiName, primaryKey); QJavalinAccessLogger.logEndSuccess(); - String resultString = JsonUtils.toJson(outputRecord); + String resultString = toJson(outputRecord); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -668,6 +669,21 @@ public class QJavalinApiHandler + /******************************************************************************* + ** Define standard way we'll make JSON objects for the API. + ** + ** Specifically, changes QQQ's default to include null values + *******************************************************************************/ + private static String toJson(Object object) + { + return JsonUtils.toJson(object, mapper -> + { + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + }); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -880,7 +896,7 @@ public class QJavalinApiHandler Map output = ApiImplementation.query(apiInstanceMetaData, version, tableApiName, context.queryParamMap()); QJavalinAccessLogger.logEndSuccess(logPair("recordCount", () -> ((List) output.get("records")).size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); - String resultString = JsonUtils.toJson(output); + String resultString = toJson(output); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -911,7 +927,7 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.CREATED.getCode()); - String resultString = JsonUtils.toJson(outputRecord); + String resultString = toJson(outputRecord); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -941,7 +957,7 @@ public class QJavalinApiHandler List> response = ApiImplementation.bulkInsert(apiInstanceMetaData, version, tableApiName, context.body()); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - String resultString = JsonUtils.toJson(response); + String resultString = toJson(response); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -972,7 +988,7 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - String resultString = JsonUtils.toJson(response); + String resultString = toJson(response); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -1003,7 +1019,7 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - String resultString = JsonUtils.toJson(response); + String resultString = toJson(response); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -1176,7 +1192,7 @@ public class QJavalinApiHandler /////////////////////////// } - String responseBody = JsonUtils.toJson(Map.of("error", errorMessage)); + String responseBody = toJson(Map.of("error", errorMessage)); context.result(responseBody); if(apiLog != null) diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 3ea783a6..6dc6f93f 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -202,6 +202,8 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(1, jsonObject.getInt("id")); assertEquals("Homer", jsonObject.getString("firstName")); assertEquals("Simpson", jsonObject.getString("lastName")); + assertTrue(jsonObject.isNull("noOfShoes")); + assertFalse(jsonObject.has("someNonField")); } @@ -376,6 +378,8 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(1, jsonObject.getInt("id")); assertEquals("Homer", jsonObject.getString("firstName")); assertEquals("Simpson", jsonObject.getString("lastName")); + assertTrue(jsonObject.isNull("noOfShoes")); + assertFalse(jsonObject.has("someNonField")); } @@ -959,7 +963,7 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(2).getInt("statusCode")); assertEquals("Error updating Person: Missing value in primary key field", jsonArray.getJSONObject(2).getString("error")); - assertFalse(jsonArray.getJSONObject(2).has("id")); + assertTrue(jsonArray.getJSONObject(2).isNull("id")); assertEquals(HttpStatus.NOT_FOUND_404, jsonArray.getJSONObject(3).getInt("statusCode")); assertEquals("Error updating Person: No record was found to update for Id = 256", jsonArray.getJSONObject(3).getString("error"));