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.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<ObjectMapper> 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<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.
**
@ -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.
**

View File

@ -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<ObjectMapper> objectMapperCustomizer)
{
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 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);
}
/*******************************************************************************
**
*******************************************************************************/

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 //
/////////////////////////////////////////////////////////////////////////////////////////////////
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<Integer>
}
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 //
///////////////////////////////////////////////////////////
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);
});
/////////////////////////////////////////////////////////////////////