From b78519aa55c2a685ac3b3b8915dbd07172b0e625 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Jul 2025 08:48:57 -0500 Subject: [PATCH] add toYamlCustomized / toJsonCustomized methods, that expose jackson's now-preferred Builder objects to be configured on instead of doing config directly on mapper objects. --- .../qqq/backend/core/utils/JsonUtils.java | 48 ++++++++++++++++++- .../qqq/backend/core/utils/YamlUtils.java | 37 +++++++++++++- .../qqq/backend/core/utils/YamlUtilsTest.java | 43 +++++++++++++++++ .../middleware/javalin/tools/PublishAPI.java | 4 +- .../javalin/tools/ValidateAPIVersions.java | 4 +- 5 files changed, 130 insertions(+), 6 deletions(-) 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 325e842f..9255be94 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 @@ -37,6 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -84,6 +85,7 @@ public class JsonUtils ** Internally using jackson - so jackson annotations apply! ** *******************************************************************************/ + @Deprecated(since = "since toJsonCustomized was added, which uses jackson's newer builder object for customization") public static String toJson(Object object, Consumer objectMapperCustomizer) { try @@ -105,6 +107,34 @@ public class JsonUtils + /******************************************************************************* + ** Serialize any object into a JSON String - with customizations on the Jackson + ** ObjectMapper. + ** + ** Internally using jackson - so jackson annotations apply! + ** + *******************************************************************************/ + public static String toJsonCustomized(Object object, Consumer jsonMapperCustomizer) + { + try + { + JsonMapper.Builder jsonMapperBuilder = newJsonMapperBuilder(); + if(jsonMapperCustomizer != null) + { + jsonMapperCustomizer.accept(jsonMapperBuilder); + } + String jsonResult = jsonMapperBuilder.build().writeValueAsString(object); + return (jsonResult); + } + catch(JsonProcessingException e) + { + LOG.error("Error serializing object of type [" + object.getClass().getSimpleName() + "] to json", e); + throw new IllegalArgumentException("Error in JSON Serialization", e); + } + } + + + /******************************************************************************* ** Serialize any object into a "pretty" / formatted JSON String. ** @@ -168,7 +198,6 @@ public class JsonUtils /******************************************************************************* ** De-serialize a json string into an object of the specified class - with ** customizations on the Jackson ObjectMapper. - **. ** ** Internally using jackson - so jackson annotations apply! ** @@ -242,6 +271,23 @@ public class JsonUtils + /******************************************************************************* + ** Standard private method to build jackson JsonMapperBuilder with standard features. + ** + *******************************************************************************/ + private static JsonMapper.Builder newJsonMapperBuilder() + { + JsonMapper.Builder jsonMapperBuilder = JsonMapper.builder(); + jsonMapperBuilder.addModule(new JavaTimeModule()); + jsonMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL); + jsonMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY); + jsonMapperBuilder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + jsonMapperBuilder.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + return (jsonMapperBuilder); + } + + /******************************************************************************* ** Standard private method to build jackson ObjectMapper with standard features. ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/YamlUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/YamlUtils.java index 23004f8d..0c0ddbd1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/YamlUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/YamlUtils.java @@ -26,9 +26,11 @@ 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.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -62,7 +64,7 @@ public class YamlUtils *******************************************************************************/ public static String toYaml(Object object) { - return toYaml(object, null); + return toYamlCustomized(object, null); } @@ -70,6 +72,7 @@ public class YamlUtils /******************************************************************************* ** *******************************************************************************/ + @Deprecated(since = "since toYamlCustomized was added, which uses jackson's newer builder object for customization") public static String toYaml(Object object, Consumer objectMapperCustomizer) { try @@ -96,4 +99,36 @@ public class YamlUtils } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String toYamlCustomized(Object object, Consumer yamlMapperCustomizer) + { + try + { + YAMLFactory yamlFactory = new YAMLFactory() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); + + YAMLMapper.Builder yamlMapperBuilder = YAMLMapper.builder(yamlFactory); + yamlMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL); + yamlMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY); + + if(yamlMapperCustomizer != null) + { + yamlMapperCustomizer.accept(yamlMapperBuilder); + } + + YAMLMapper yamlMapper = yamlMapperBuilder.build(); + yamlMapper.findAndRegisterModules(); + return (yamlMapper.writeValueAsString(object)); + } + catch(Exception e) + { + LOG.error("Error serializing object of type [" + object.getClass().getSimpleName() + "] to yaml", e); + throw new IllegalArgumentException("Error in YAML Serialization", e); + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/YamlUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/YamlUtilsTest.java index 3c1c7c95..3e715cf8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/YamlUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/YamlUtilsTest.java @@ -24,9 +24,11 @@ package com.kingsrook.qqq.backend.core.utils; import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /******************************************************************************* @@ -60,6 +62,47 @@ class YamlUtilsTest extends BaseTest + /*************************************************************************** + * simple bean to use in customObjectMapper test + * we'd use a map, but SORT_PROPERTIES_ALPHABETICALLY doesn't apply to maps... + ***************************************************************************/ + private record SomeBean(String foo, Integer bar) {} + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCustomObjectMapper() throws JsonProcessingException + { + SomeBean someBean = new SomeBean("Hi", 47); + + /////////////////////////////////////////////////////////////////// + // by default, the fields come out in the order they're declared // + /////////////////////////////////////////////////////////////////// + assertEquals(""" + foo: "Hi" + bar: 47 + """, YamlUtils.toYaml(someBean)); + + ///////////////////////////////////////////////////////////// + // customize the builder to sort properties alphabetically // + // (to assert that doing a customization works) // + ///////////////////////////////////////////////////////////// + String outputYaml = YamlUtils.toYamlCustomized(someBean, yamlMapperBuilder -> + { + yamlMapperBuilder.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + }); + + assertEquals(""" + bar: 47 + foo: "Hi" + """, outputYaml); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java index a411c403..b2216ba4 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java @@ -104,7 +104,7 @@ public class PublishAPI implements Callable // subsets of it (e.g., grouped by table mostly) - then we'll write out each such file // ///////////////////////////////////////////////////////////////////////////////////////////////// OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); - String yaml = YamlUtils.toYaml(openAPI, mapper -> + String yaml = YamlUtils.toYamlCustomized(openAPI, mapperBuilder -> { if(sortFileContentsForHuman) { @@ -114,7 +114,7 @@ public class PublishAPI implements Callable } else { - mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + mapperBuilder.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); } }); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java index fdc918ae..d2d83bdd 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java @@ -112,9 +112,9 @@ public class ValidateAPIVersions implements Callable // generate a new spec based on current code in codebase // /////////////////////////////////////////////////////////// OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); - String yaml = YamlUtils.toYaml(openAPI, mapper -> + String yaml = YamlUtils.toYamlCustomized(openAPI, mapperBuilder -> { - mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + mapperBuilder.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); }); /////////////////////////////////////////////////////////////////////