Core path ยท 5 of 34
Learn how Sling Models bridge content and presentation. Understand adaptation, dependency injection, model lifecycle, and why they're essential for maintainable AEM components. Discover how Sling Models enable both traditional and headless AEM.
In this chapter, you'll discover:
In previous chapters of this series, we followed a request through several stages of the Sling framework.
We learned:
At this point, Sling has successfully:
But there's a gap we haven't addressed yet:
Key question:
Consider a Title component.
"Welcome To AEM"<h1>Welcome To AEM</h1>Something has to connect those pieces โ retrieve the stored value, maybe validate or transform it, and hand it to HTL in a shape the template can work with.
That something is Sling Models.
Sling Models provide a clean way to adapt Sling objects (Resources and Requests) into Java objects that can be used by HTL.
Instead of writing Java code inside presentation templates, business logic is moved into dedicated model classes that are easier to maintain, test, and reuse.

Sling Models sit between the resource layer and the presentation layer.
Before Sling Models became popular, developers often mixed rendering and business logic together.
Templates were responsible not only for displaying data but also for:
As applications grew, this approach became difficult to maintain and difficult to test.
Modern AEM development follows a cleaner separation of responsibilities:
Resources โ Provide content
Sling Models โ Provide business logic
HTL โ Provide presentation
Each layer focuses on a specific responsibility, making components:
A typical rendering flow looks like this:
Resource (Content)
โ
Sling Model (Logic)
โ
HTL (Presentation)
โ
HTML (Output)
This separation is one of the reasons Sling Models became the recommended approach for modern AEM component development.
Consider a requirement where a title must always be displayed in uppercase.
Wrong approach:
<h1>${properties.jcr:title.toUpperCase()}</h1>
Problems:
Correct approach:
@Model(adaptables = Resource.class)
public class TitleModel {
@ValueMapValue
private String title;
public String getDisplayTitle() {
return title != null ? title.toUpperCase() : "";
}
}
HTL then remains focused on rendering:
<h1>${model.displayTitle}</h1>
Benefits:
HTL remains focused on rendering while the model remains responsible for preparing the data.
This is fundamental to how Sling Models work.
Adaptation is the process of transforming one object type into another.
Example:
Resource resource =
resourceResolver.getResource("/content/site/en/home/jcr:content/root/title");
TitleModel model =
resource.adaptTo(TitleModel.class);
if (model != null) {
String title = model.getTitle();
}
Breakdown:
Resource
โ
adaptTo(TitleModel.class)
โ
TitleModel (Java Object)
Adaptation is not guaranteed to succeed.
If Sling cannot find a suitable adapter, adaptTo() returns null.
TitleModel model = resource.adaptTo(TitleModel.class);
if (model == null) {
// Adaptation failed - Sling couldn't create the model
// Possible reasons:
// - Model class doesn't exist
// - Required fields couldn't be injected
// - Model registration failed
// - Resource type doesn't match
}
This is why null checks remain important when working directly with adaptation APIs.
Adaptation does not create random objects.
Instead, Sling uses registered adapters to transform one object type into another.
When you annotate a class with @Model:
@Model(adaptables = Resource.class)
public class TitleModel {
// ...
}
You're essentially saying: "Register me as an adapter that can transform a Resource into a TitleModel."
Sling then maintains a registry of adapters. When adaptTo() is called:
1. Check if there's a registered adapter
Resource โ TitleModel?
2. If yes, use it
3. If no, return null
One of the most misunderstood concepts in Sling Models.

@Model(adaptables = Resource.class)
public class TitleModel {
@ValueMapValue
private String title;
public String getTitle() {
return title;
}
}
Use Resource adaptables when:
Examples:
@Model(adaptables = SlingHttpServletRequest.class)
public class DynamicPageModel {
@Inject
private SlingHttpServletRequest request;
public String getSelector() {
return request.getRequestPathInfo().getSelectors()[0];
}
public String getSuffix() {
return request.getRequestPathInfo().getSuffix();
}
}
Use Request adaptables when:
Examples:
| Aspect | Resource Adaptable | Request Adaptable |
|---|---|---|
| Data Source | Content properties | Request information |
| Use Case | Static content components | Dynamic/parameterized components |
| Access to Selectors | No | Yes |
| Access to Request Parameters | No | Yes |
| Access to Headers | No | Yes |
| Typically Simpler | Yes | No |
Sling Models use annotations to inject dependencies into model fields.
Read properties from the Resource's ValueMap:
@Model(adaptables = Resource.class)
public class ArticleModel {
@ValueMapValue
private String title; // Reads jcr:title
@ValueMapValue(name = "jcr:description")
private String description; // Custom property name
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
}
How it works:
Resource
โ
ValueMap
โ
@ValueMapValue fields populated
Access resources nested under the current resource:
@Model(adaptables = Resource.class)
public class ContainerModel {
@ChildResource
private Resource image; // Access child resource named "image"
public Resource getImage() {
return image;
}
}
Repository structure:
/content/site/home/jcr:content/root/container
โโ image (child resource)
โโ title
โโ description
Inject OSGi services into your model:
@Model(adaptables = Resource.class)
public class EmailModel {
@OSGiService
private EmailService emailService;
@OSGiService(filter = "slingFilterName=admin")
private SlingFilter adminFilter;
public void sendEmail(String to, String subject) {
emailService.send(to, subject, "Body");
}
}
This allows models to:
Inject commonly used Sling objects:
@Model(adaptables = SlingHttpServletRequest.class)
public class DynamicModel {
@SlingObject
private ResourceResolver resourceResolver;
@SlingObject
private SlingHttpServletRequest request;
@SlingObject
private Resource resource;
public String getResourcePath() {
return resource.getPath();
}
public Resource loadOtherResource(String path) {
return resourceResolver.getResource(path);
}
}
Understanding the model lifecycle is critical.
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String title;
@ValueMapValue
private Double price;
@OSGiService
private PricingService pricingService;
private Double discountedPrice;
@PostConstruct
private void init() {
// This runs AFTER all injections are complete
if (price != null) {
// Apply discount logic
discountedPrice = pricingService.applyDiscount(price);
}
}
public String getTitle() {
return title;
}
public Double getDiscountedPrice() {
return discountedPrice;
}
}
@ValueMapValue
private String title; // Injected by Sling
โ
@PostConstruct
private void init() { // Called by Sling after injection
if (title != null) {
// Now we can safely use injected fields
pageTitle = title.toUpperCase();
}
}
Important: All injections happen before @PostConstruct executes.
This means:
Let's trace a Title component from author to HTML.
/content/site/en/home/jcr:content/root/title
jcr:title = "Welcome To AEM"
sling:resourceType = "company/components/title"
@Model(adaptables = Resource.class)
public class TitleModel {
@ValueMapValue
private String title;
@PostConstruct
private void init() {
if (title != null) {
title = title.trim();
}
}
public String getTitle() {
return title != null ? title : "No Title";
}
}
<h1>${model.title}</h1>
// WRONG
TitleModel model = resource.adaptTo(TitleModel.class);
String title = model.getTitle(); // NullPointerException if adaptation failed!
Correct:
TitleModel model = resource.adaptTo(TitleModel.class);
if (model != null) {
String title = model.getTitle();
}
// RISKY - throws exception if field is missing
@ValueMapValue
private String optionalField;
Better:
// Safe - uses default value if missing
@ValueMapValue
private String optionalField = "default";
Or specify required:
// Throws if required and missing
@ValueMapValue(required = true)
private String requiredField;
// WRONG - relying on getter to handle null
public String getTitle() {
if (title == null) {
return "No Title"; // Checked on every call
}
return title;
}
Better:
@PostConstruct
private void init() {
if (title == null) {
title = "No Title"; // Set once during initialization
}
}
public String getTitle() {
return title; // Always guaranteed to have a value
}
// WRONG - trying to use Request object on Resource adaptable
@Model(adaptables = Resource.class)
public class MyModel {
@Inject
private SlingHttpServletRequest request; // Will be null!
}
Correct:
@Model(adaptables = SlingHttpServletRequest.class)
public class MyModel {
@Inject
private SlingHttpServletRequest request; // Will be injected
}
When a component renders but shows no content:
| Check | What to Verify |
|---|---|
| Resource exists | Did ResourceResolver find the content? |
| Model adapts | Does resource.adaptTo(Model.class) return non-null? |
| Fields injected | Are @ValueMapValue fields being populated? |
| @PostConstruct runs | Does init() execute without errors? |
| HTL uses model | Does HTL access ${model} correctly? |
@Model(adaptables = Resource.class)
public class DebugModel {
@ValueMapValue
private String title;
@PostConstruct
private void init() {
// Add logging to debug
if (title == null) {
System.out.println("WARNING: title is null!");
}
}
public String getTitle() {
return title != null ? title : "(empty title)";
}
}
Check logs to see if field injection succeeded.
Sling Models are not limited to HTL rendering.
They're also heavily used by the AEM JSON Exporter.
When a request like this executes:
/content/site/en/home.model.json
Sling does the following:
Request parsed
โ
Extension = json, Selector = model
โ
ResourceResolver finds resource
โ
Sling adapts resource to model
โ
JSON exporter serializes model to JSON
โ
Response sent as JSON
Result:
{
"title": "Welcome To AEM",
"description": "Our homepage",
"image": "image.jpg"
}
The same Sling Model can support both traditional and headless rendering:
/home.html โ HTL rendering
/home.model.json โ JSON export (Headless)
/home.model.xml โ XML export
For teams building React, Next.js, or mobile applications on top of AEM, Sling Models often become the foundation of the API layer.
Here's the end-to-end flow combining everything we've learned:

Experienced AEM developers don't think of Sling Models as simple data containers.
They view Sling Models as the business layer between content and presentation.
Resources โ Content Storage
Sling Models โ Business Logic (HERE)
HTL โ Presentation
When troubleshooting component rendering issues, architects often verify the model layer before investigating HTL because many rendering problems originate from:
If the model isn't adapting correctly, there's no point investigating HTL.
In this chapter, we learned how Sling Models adapt Resources into business objects that HTL can consume.
We've now covered the complete Sling rendering pipeline:
You now understand the complete architecture behind every page rendered in AEM.
From here, you're ready for advanced topics:
The foundation is complete. The advanced topics will build naturally on top of it.
Read Real-World Scenario โ When a Sling Model Earns Its Place (and When ValueMap Is Enough) for the null-adaptation and over-injected-model problems that show up once a project has dozens of models, plus honest criteria for when raw resource/ValueMap access is simpler.
Also see Real World Scenarios: The Optional Field That Wasn't Optional for a concrete NullPointerException that comes from a very common gap: an @Optional injected field handled correctly in @PostConstruct's "present" branch, but with no fallback for the "absent" branch โ and why the fix belongs in both the initializer and the getter, not just one of them.
Get an email when I publish the next chapter. No spam โ just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.