add toYamlCustomized / toJsonCustomized methods, that expose jackson's now-preferred Builder objects to be configured on instead of doing config directly on mapper objects.

This commit is contained in:
2025-07-15 08:48:57 -05:00
parent 01f1e074e2
commit b78519aa55
5 changed files with 130 additions and 6 deletions

View File

@ -37,6 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider; 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.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -84,6 +85,7 @@ public class JsonUtils
** Internally using jackson - so jackson annotations apply! ** 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<ObjectMapper> objectMapperCustomizer) public static String toJson(Object object, Consumer<ObjectMapper> objectMapperCustomizer)
{ {
try 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<JsonMapper.Builder> 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. ** 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 ** De-serialize a json string into an object of the specified class - with
** customizations on the Jackson ObjectMapper. ** customizations on the Jackson ObjectMapper.
**.
** **
** Internally using jackson - so jackson annotations apply! ** 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. ** Standard private method to build jackson ObjectMapper with standard features.
** **

View File

@ -26,9 +26,11 @@ import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -62,7 +64,7 @@ public class YamlUtils
*******************************************************************************/ *******************************************************************************/
public static String toYaml(Object object) 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<ObjectMapper> objectMapperCustomizer) public static String toYaml(Object object, Consumer<ObjectMapper> objectMapperCustomizer)
{ {
try try
@ -96,4 +99,36 @@ public class YamlUtils
} }
} }
/*******************************************************************************
**
*******************************************************************************/
public static String toYamlCustomized(Object object, Consumer<YAMLMapper.Builder> 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);
}
}
} }

View File

@ -24,9 +24,11 @@ package com.kingsrook.qqq.backend.core.utils;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException; 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.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import org.junit.jupiter.api.Test; 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);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -104,7 +104,7 @@ public class PublishAPI implements Callable<Integer>
// subsets of it (e.g., grouped by table mostly) - then we'll write out each such file // // subsets of it (e.g., grouped by table mostly) - then we'll write out each such file //
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq");
String yaml = YamlUtils.toYaml(openAPI, mapper -> String yaml = YamlUtils.toYamlCustomized(openAPI, mapperBuilder ->
{ {
if(sortFileContentsForHuman) if(sortFileContentsForHuman)
{ {
@ -114,7 +114,7 @@ public class PublishAPI implements Callable<Integer>
} }
else else
{ {
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); mapperBuilder.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
} }
}); });

View File

@ -112,9 +112,9 @@ public class ValidateAPIVersions implements Callable<Integer>
// generate a new spec based on current code in codebase // // generate a new spec based on current code in codebase //
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); 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);
}); });
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////