How to Simplify AEM Configuration Serialization

The GenericConfigSerializer is a Jackson serializer designed to automatically handle the serialization of Context-Aware Configurations (CAC) objects.


This content originally appeared on HackerNoon and was authored by Giuseppe Baglio

Finally, time for coffee breaks!

\

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.

\ Relaxed fellow programmer after using code from this article

\

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

\


This content originally appeared on HackerNoon and was authored by Giuseppe Baglio


Print Share Comment Cite Upload Translate Updates
APA

Giuseppe Baglio | Sciencx (2025-01-24T01:35:48+00:00) How to Simplify AEM Configuration Serialization. Retrieved from https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/

MLA
" » How to Simplify AEM Configuration Serialization." Giuseppe Baglio | Sciencx - Friday January 24, 2025, https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/
HARVARD
Giuseppe Baglio | Sciencx Friday January 24, 2025 » How to Simplify AEM Configuration Serialization., viewed ,<https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/>
VANCOUVER
Giuseppe Baglio | Sciencx - » How to Simplify AEM Configuration Serialization. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/
CHICAGO
" » How to Simplify AEM Configuration Serialization." Giuseppe Baglio | Sciencx - Accessed . https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/
IEEE
" » How to Simplify AEM Configuration Serialization." Giuseppe Baglio | Sciencx [Online]. Available: https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/. [Accessed: ]
rf:citation
» How to Simplify AEM Configuration Serialization | Giuseppe Baglio | Sciencx | https://www.scien.cx/2025/01/24/how-to-simplify-aem-configuration-serialization/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.