Introduction
Managing Context-Aware Configurations¹ serialization in Adobe Experience Manager (AEM) can be a nightmare. From handling nested² Context-Aware Configurations to writing repetitive boilerplate code, developers often find themselves bogged down by complexity.
Today, we’ll explore a smart solution that simplifies this process: the GenericConfigSerializer. This custom Jackson serializer provides a powerful, flexible approach to serializing Context-Aware Configurations with minimal boilerplate code, making it easier to maintain and scale your AEM applications.
Understanding the Serialization Challenge
In AEM, Context-Aware Configurations (CAC) provide a flexible way to manage settings across different contexts, such as environments, languages, or regions. However, serializing these configurations can be a daunting task. Traditional serialization methods often fall short in three key areas:
- Manual mapping madness 😵💫: They require manual mapping of configuration properties, which can lead to tedious, error-prone code that’s hard to maintain.
- Nested complexity 🕸️: They struggle to handle complex, nested configuration hierarchies, making it difficult to serialize and deserialize configurations accurately. Refer to the Perficient blog post for a detailed explanation of nested Context-Aware Configurations in AEM².
- Verbose code 📜: They create verbose and repetitive serialization code that clutters your codebase and makes it harder to focus on the logic that matters.
Introducing the GenericConfigSerializer
The GenericConfigSerializer is a generic Jackson serializer designed to automatically handle the serialization of Context-Aware Configurations (CAC) objects.
For more information on Sling Model exporters, see the Adobe Experience League documentation³ and the Apache Sling documentation⁴. Here’s how the serializer works under the hood:
- A generic serializer class that extends Jackson’s
StdSerializer
class.
- A set of methods that handle the serialization of different data types, such as simple properties, arrays, and nested proxy objects.
The serializer’s code is as follows:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* A generic serializer for configuration objects.
*
* <p>This serializer handles the serialization of Context-Aware Configurations
* objects by iterating over their methods and serializing the properties
* accordingly.
* It supports serialization of arrays and proxy objects.</p>
*
* @param <T> the type of the configuration object
*/
@Slf4j
public class GenericConfigSerializer<T> extends StdSerializer<T> {
public GenericConfigSerializer() {
this(null);
}
/**
* Constructor with class type.
*
* @param t the class type of the object to serialize
*/
public GenericConfigSerializer(Class<T> t) {
super(t);
}
/**
* Serializes the given value.
*
* @param value the value to serialize
* @param gen the JSON generator
* @param provider the serializer provider
* @throws IOException if an I/O error occurs
*/
@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider provider) {
try {
gen.writeStartObject();
for (Method method : getConfigPropertyMethods(value)) {
serializeMethod(value, gen, provider, method);
}
gen.writeEndObject();
} catch (IOException e) {
log.error("Impossible to serialize value: {}", value, e);
}
}
/**
* Retrieves the configuration property methods.
*
* @param value the configuration object
* @return a list of configuration property methods
*/
private List<Method> getConfigPropertyMethods(T value) {
return Arrays.stream(value.getClass().getInterfaces()[0].getDeclaredMethods())
.filter(this::isConfigPropertyMethod).filter(this::isNotIgnoredPropertyMethod)
.collect(Collectors.toList());
}
/**
* Serializes a single method.
*
* @param value the configuration object
* @param gen the JSON generator
* @param provider the serializer provider
* @param method the method to serialize
* @throws IOException if an I/O error occurs
*/
private void serializeMethod(T value, JsonGenerator gen, SerializerProvider provider, Method method) {
try {
Object returnValue = method.invoke(value);
if (returnValue != null) {
String fieldName = getJsonPropertyName(method);
serializeField(gen, provider, fieldName, returnValue);
}
} catch (InvocationTargetException | IllegalAccessException e) {
log.error("Impossible to serialize method {} (value: {})", method, value, e);
}
}
/**
* Serializes a field.
*
* @param gen the JSON generator
* @param provider the serializer provider
* @param fieldName the name of the field
* @param returnValue the value of the field
* @throws IOException if an I/O error occurs
*/
private void serializeField(JsonGenerator gen, SerializerProvider provider, String fieldName, Object returnValue) {
try {
if (returnValue.getClass().isArray()) {
serializeArray(gen, provider, fieldName, (Object[]) returnValue);
} else if (returnValue.getClass().getName().contains("com.sun.proxy")) {
gen.writeFieldName(fieldName);
serialize((T) returnValue, gen, provider);
} else {
gen.writeObjectField(fieldName, returnValue);
}
} catch (IOException e) {
log.error("Impossible to serialize value: {}; fieldName: {}", returnValue, fieldName, e);
}
}
/**
* Serializes an array field.
*
* @param gen the JSON generator
* @param provider the serializer provider
* @param fieldName the name of the field
* @param array the array to serialize
* @throws IOException if an I/O error occurs
*/
private void serializeArray(JsonGenerator gen, SerializerProvider provider, String fieldName, Object[] array) {
try {
gen.writeArrayFieldStart(fieldName);
for (Object item : array) {
if (item.getClass().getName().contains("com.sun.proxy")) {
serialize((T) item, gen, provider);
} else {
gen.writeObject(item);
}
}
gen.writeEndArray();
} catch (IOException e) {
log.error("Impossible to serialize array: {}; fieldName: {}", array, fieldName, e);
}
}
/**
* Determines if a method is a configuration property method.
*
* @param method the method to check
* @return true if the method is a configuration property method, false otherwise
*/
private boolean isConfigPropertyMethod(Method method) {
// Filter out methods that are not configuration properties
return method.getParameterCount() == 0 && method.getReturnType() != void.class &&
!method.getName().equals("annotationType") &&
!method.getName().equals("hashCode");
}
/**
* Determines if a method is ignored based on the @JsonIgnore annotation.
*
* @param method the method to check
* @return true if the method is ignored, false otherwise
*/
private boolean isNotIgnoredPropertyMethod(Method method) {
return method.getAnnotation(JsonIgnore.class) == null;
}
/**
* Gets the JSON property name from the @JsonProperty annotation if present.
*
* @param method the method to check
* @return the JSON property name
*/
private String getJsonPropertyName(Method method) {
JsonProperty jsonProperty = method.getAnnotation(JsonProperty.class);
if (jsonProperty != null && !jsonProperty.value().isEmpty()) {
return jsonProperty.value();
}
return method.getName();
}
}
Illustrating the Serializer With a Product Catalog Configuration
This example illustrates how to use the GenericConfigSerializer with a fictional product catalog configuration. It demonstrates how the serializer can simplify the serialization of complex configurations.
@JsonSerialize(using = GenericConfigSerializer.class)
@Configuration(label = "Product Catalog Settings")
public @interface ProductCatalogConfig {
@Property(label = "Enable Personalization", description = "Toggle personalized product recommendations")
@JsonIgnore
boolean enablePersonalization();
@Property(label = "Default Currency", description = "Standard currency for product pricing")
String defaultCurrency();
@Property(label = "Inventory Threshold", description = "Low stock warning level")
int lowStockThreshold();
@Property(label = "Shipping Configurations")
ShippingConfig shippingConfig();
}
@Configuration(label = "Shipping Configuration")
public @interface ShippingConfig {
@Property(label = "Free Shipping Minimum")
@JsonProperty("minFreeShipping")
double freeShippingMinimum();
@Property(label = "Supported Shipping Regions")
String[] supportedRegions();
@Property(label = "Express Shipping Enabled")
boolean expressShippingEnabled();
}
Class ShippingConfig
could have been written to have nested complex classes at multiple levels of depth, allowing for a more intricate and hierarchical structure. However, for the sake of exposition, I am keeping it relatively simple with only one level of depth to focus on illustrating the concept of using the GenericConfigSerializer.
As shown in the code above, only the ProductCatalogConfig
class has been annotated with @JsonSerialize
, which instructs the out-of-the-box AEM Jackson serialization to leverage the GenericConfigSerializer to serialize the object.
When serialized, the configuration might look like:
{
"defaultCurrency": "USD",
"lowStockThreshold": 10,
"shippingConfig": {
"minFreeShipping": 50.00,
"supportedRegions": ["US", "CA", "UK"],
"expressShippingEnabled": true
}
}
The GenericConfigSerializer leverages reflection⁵ techniques to transform configuration serialization. By analyzing interface methods, it identifies and processes the configuration properties with precision and flexibility.
The serialization automatically excludes methods with parameters and ignores standard Java object methods like hashCode()
, ensuring only relevant getter methods are serialized.
Annotation-based customization provides developers with granular control. The @JsonIgnore
annotation allows selective method exclusion while @JsonProperty
enabling custom property naming. This flexibility ensures the serializer adapts to complex configuration structures without compromising code clarity.
How to Serialize Product Catalog in Sling Models
Exposing Context-Aware Configurations (CAC) in Sling Models often involves tedious, manual property mapping. Developers would typically write repetitive code to extract and map each configuration attribute individually, leading to verbose and maintenance-heavy implementations.
Let’s take as an example the following Sling Model:
@Model(adaptables = SlingHttpServletRequest.class)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ProductPageModel {
private String defaultCurrency;
private boolean personalizationEnabled;
private int lowStockThreshold;
private double freeShippingMinimum;
private String[] supportedRegions;
@PostConstruct
public void init() {
ProductCatalogConfig config = getCAConfig(resource)
.orElse(null);
if (config != null) {
this.defaultCurrency = config.defaultCurrency();
this.personalizationEnabled = config.enablePersonalization();
this.lowStockThreshold = config.lowStockThreshold();
// Nested configuration handling
ShippingConfig shippingConfig = config.shippingConfig();
if (shippingConfig != null) {
this.freeShippingMinimum = shippingConfig.freeShippingMinimum();
this.supportedRegions = shippingConfig.supportedRegions();
}
}
}
// Getter methods for each property
public String getDefaultCurrency() { return defaultCurrency; }
public boolean isPersonalizationEnabled() { return personalizationEnabled; }
public int getLowStockThreshold() { return lowStockThreshold; }
public double getFreeShippingMinimum() { return freeShippingMinimum; }
public String[] getSupportedRegions() { return supportedRegions;
private ProductCatalogConfig getCAConfig(Resource resource) {
if (resource == null) {
return null;
}
ConfigurationBuilder configBuilder = resource.adaptTo(ConfigurationBuilder.class);
if (configBuilder == null) {
return null;
}
return configBuilder.as(ProductCatalogConfig.class);
}
}
Looking at this code, you’ll notice how each field from the Product Catalog Configuration must be manually mapped to local class fields within the init method. This one-to-one mapping approach quickly becomes unsustainable as configurations grow in complexity. As your Product Catalog Configuration expands to include more fields and nested structures, the mapping code grows proportionally, leading to:
- Redundant boilerplate that obscures the actual business logic
- Time-consuming updates when configuration structures change
- Higher risk of mapping errors during implementation
- Increased cognitive load when reading and maintaining the code
The resulting JSON export might look like:
{
"defaultCurrency": "USD",
"enablePersonalization": true,
"lowStockThreshold": 10,
"shippingConfig": {
"freeShippingMinimum": 50.00,
"supportedRegions": ["US", "CA", "UK"],
"expressShippingEnabled": true
}
}
With the GenericConfigSerializer, we can simplify the model in a few lines:
@Model(adaptables = SlingHttpServletRequest.class)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ProductPageModel {
private ProductCatalogConfig productConfig;
@PostConstruct
public void init() {
this.productConfig = getCAConfig(resource);
}
private ProductCatalogConfig getCAConfig(Resource resource) {
// same code as before
}
// Expose the entire configuration for automatic serialization
public ProductCatalogConfig getProductConfig() {
return productConfig;
}
}
The resulting JSON export might look like:
{
"productConfig": {
"defaultCurrency": "USD",
"enablePersonalization": true,
"lowStockThreshold": 10,
"shippingConfig": {
"freeShippingMinimum": 50.00,
"supportedRegions": ["US", "CA", "UK"],
"expressShippingEnabled": true
}
}
}
Using the GenericConfigSerializer, developers can simplify their Sling Models, reduce boilerplate code, and improve maintainability. The serializer’s flexibility and customizability make it an ideal solution for handling complex configurations in AEM.
Key Transformation Highlights
The real power of GenericConfigSerializer becomes evident when we look at its transformative impact. By eliminating manual property mapping, the approach dramatically simplifies configuration handling. Developers can now rely on automatic serialization that effortlessly manages configurations without requiring explicit mapping for each attribute.
One of the most compelling benefits is how seamlessly it handles nested configurations. Complex, multi-layered configuration structures that once demanded intricate manual parsing are now easily processed. The serializer intelligently navigates through configuration hierarchies, reducing the cognitive load on developers.
Conclusion
The solution significantly reduces boilerplate code, transforming what was once a verbose implementation into concise, elegant code. This lean approach not only makes the code more readable but also more maintainable. Configuration management becomes far more flexible, allowing developers to modify and adapt configurations without getting entangled in complex serialization logic.
By leveraging the GenericConfigSerializer, developers can unlock a more efficient and streamlined AEM configuration management approach. With its ability to automatically serialize configurations, handle nested configurations, and eliminate manual property mapping, this solution is a game-changer for any AEM developer looking to simplify their workflow and improve their productivity.
Honestly, I created the GenericConfigSerializer for one reason: I’m lazy. I hate writing the same code repeatedly, and I’m sure I’m not the only one. But as I started using the serializer in my projects, I realized it could be a game-changer for everyone, not just me. So, I decided to share it with the community.
Whether you’re building a new AEM project or optimizing an existing one, the GenericConfigSerializer is valuable in your toolkit. Its flexibility and ease of use make it an ideal solution for developers of all levels, from beginners to seasoned experts. By adopting the GenericConfigSerializer, you can take your AEM development to the next level and deliver high-quality solutions faster and more efficiently.
Ready to simplify your AEM configuration serialization? Try the GenericConfigSerializer in your next project and experience the difference. Share your feedback in the comments below!
[¹] Apache Sling Context-Aware Configuration documentation: https://sling.apache.org/documentation/bundles/context-aware-configuration/context-aware-configuration.html
[²] As projects grow, configuration needs can become intricate, requiring nested configurations to handle multi-layered settings, as discussed in a blog post by Perficient
[³] Adobe Experience League documentation: https://experienceleague.adobe.com/en/docs/experience-manager-learn/foundation/development/understand-sling-model-exporter
[⁴] Apache Sling Exporter Framework documentation: https://sling.apache.org/documentation/bundles/models.html#exporter-framework-1
[⁵] Java Reflection API: https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html