Skip to main content

Sling Models

Sling Models are the bridge between the JCR and your Java code. They let you annotate a POJO (Plain Old Java Object) so that Sling automatically injects content properties, child resources, and OSGi services. Every non-trivial component should have a Sling Model.

Basic Sling Model

package com.mysite.core.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

@Model(adaptables = Resource.class)
public class TitleModel {

@ValueMapValue
@Default(values = "Default Title")
private String title;

@ValueMapValue
@Default(values = "h2")
private String headingLevel;

public String getTitle() {
return title;
}

public String getHeadingLevel() {
return headingLevel;
}
}

Used in HTL:

<div data-sly-use.model="com.mysite.core.models.TitleModel">
<h2 data-sly-element="${model.headingLevel}">${model.title}</h2>
</div>

Adaptables -- Resource vs SlingHttpServletRequest

The adaptables parameter defines what the model can be adapted from:

AdaptableUse whenAvailable injectors
Resource.classYou only need resource properties and children@ValueMapValue, @ChildResource, @ResourcePath
SlingHttpServletRequest.classYou need the request, session, selectors, or request attributesAll of the above plus @RequestAttribute, @ScriptVariable, @Self
// Resource-based (simpler, preferred when sufficient)
@Model(adaptables = Resource.class)
public class SimpleModel { ... }

// Request-based (more powerful)
@Model(adaptables = SlingHttpServletRequest.class)
public class RequestAwareModel { ... }

Best practice: Use Resource.class when possible. It is simpler, more testable, and works in non-request contexts (e.g., background jobs).

Injection annotations

@ValueMapValue -- read properties

Injects a JCR property from the resource's ValueMap:

@ValueMapValue
private String title; // reads ./title

@ValueMapValue
private boolean featured; // reads ./featured

@ValueMapValue
private Calendar publishDate; // reads ./publishDate

@ValueMapValue(name = "jcr:title")
private String pageTitle; // reads ./jcr:title (specify name when it differs)

Supported types: String, Boolean/boolean, Integer/int, Long/long, Double/double, Calendar, String[], and more.

@ChildResource -- read child nodes

Injects child resources (useful for multifields):

@ChildResource
private List<Resource> links; // reads ./links child node's children

@ChildResource(name = "socialLinks")
private List<Resource> socials; // reads ./socialLinks children

For typed multifield items, create a nested model:

@Model(adaptables = Resource.class)
public class LinkItem {
@ValueMapValue
private String label;

@ValueMapValue
private String url;

@ValueMapValue
@Default(booleanValues = false)
private boolean openInNewTab;

public String getLabel() { return label; }
public String getUrl() { return url; }
public boolean isOpenInNewTab() { return openInNewTab; }
}

Then inject the list:

@Model(adaptables = Resource.class)
public class NavigationModel {

@ChildResource
private List<LinkItem> links;

public List<LinkItem> getLinks() {
return links != null ? links : Collections.emptyList();
}
}

@Self -- inject the adaptable itself

@Model(adaptables = Resource.class)
public class PageModel {

@Self
private Resource resource;

public String getPath() {
return resource.getPath();
}
}

With request-based models, @Self gives you the request:

@Model(adaptables = SlingHttpServletRequest.class)
public class SearchModel {

@Self
private SlingHttpServletRequest request;

public String getQuery() {
return request.getParameter("q");
}
}

Security note: Request parameters are untrusted input. Validate and normalize values before using them in queries, external calls, redirects, or authorization decisions.

@OSGiService -- inject OSGi services

@Model(adaptables = Resource.class)
public class ArticleListModel {

@OSGiService
private QueryBuilder queryBuilder;

@Self
private Resource resource;

public List<Article> getArticles() {
ResourceResolver resolver = resource.getResourceResolver();
// Use queryBuilder to find articles...
return articles;
}
}

@ScriptVariable -- inject HTL global objects

Only available with SlingHttpServletRequest adaptable:

@Model(adaptables = SlingHttpServletRequest.class)
public class PageHeaderModel {

@ScriptVariable
private Page currentPage;

@ScriptVariable
private Style currentStyle;

public String getPageTitle() {
return currentPage.getTitle();
}
}

@RequestAttribute -- read parameters from HTL

When HTL passes parameters to the model:

<div data-sly-use.model="${'com.mysite.core.models.ListModel' @ maxItems=5}">
@Model(adaptables = SlingHttpServletRequest.class)
public class ListModel {

@RequestAttribute
@Default(intValues = 10)
private int maxItems;
}

@ResourcePath -- inject a resource by path

@Model(adaptables = Resource.class)
public class FooterModel {

@ResourcePath(path = "/content/mysite/en/jcr:content")
private Resource siteRoot;

public String getSiteName() {
ValueMap props = siteRoot.getValueMap();
return props.get("jcr:title", "My Site");
}
}

@Via -- control the injection source

When your model adapts from SlingHttpServletRequest but you want a specific injector to read from the underlying resource instead, use @Via:

@Model(adaptables = SlingHttpServletRequest.class)
public class HybridModel {

@ValueMapValue
@Via("resource")
private String title;

@Self
private SlingHttpServletRequest request;
}

@Via("resource") tells the injector to resolve the value from the request's resource rather than the request itself. This is useful when you need both request-level features (selectors, request attributes) and resource-level property injection in the same model.

@PostConstruct -- initialization logic

Run logic after all injections are complete:

@Model(adaptables = Resource.class)
public class ArticleModel {

@ValueMapValue
private String title;

@ValueMapValue
private String text;

private int readingTime;

@PostConstruct
protected void init() {
if (text != null) {
int wordCount = text.split("\\s+").length;
readingTime = Math.max(1, wordCount / 200);
}
}

public int getReadingTime() {
return readingTime;
}
}

@PostConstruct is called once, after construction and injection. Use it for:

  • Computing derived values
  • Validation
  • Initializing complex state

Optional injection

By default, Sling Model injection is required -- if a property does not exist, the model fails to adapt ( adaptTo() returns null). To make injection optional:

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
private String title; // null if missing, no exception

Or at the model level (recommended):

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MyModel {
// All injections are optional by default
@ValueMapValue
private String title; // null if missing, no exception

@ValueMapValue
private String text; // null if missing, no exception
}

Best practice: Use DefaultInjectionStrategy.OPTIONAL at the model level and provide @Default values for fields that need them. This prevents exceptions when authors have not filled in all fields. Well, theoretically, DefaultInjectionStrategy.REQUIRED would be ideal, however, in daily development, this most often leads to quite a bit of pain.

Interface-based models with adapters

For better encapsulation, define an interface and use adapters:

// Interface (public API)
public interface Hero {
String getHeading();
String getSubheading();
String getImagePath();
String getCtaLabel();
String getCtaLink();
}
// Implementation
@Model(
adaptables = Resource.class,
adapters = Hero.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class HeroImpl implements Hero {

@ValueMapValue
private String heading;

@ValueMapValue
private String subheading;

@ValueMapValue(name = "fileReference")
private String imagePath;

@ValueMapValue
private String ctaLabel;

@ValueMapValue
private String ctaLink;

@Override
public String getHeading() { return heading; }

@Override
public String getSubheading() { return subheading; }

@Override
public String getImagePath() { return imagePath; }

@Override
public String getCtaLabel() { return ctaLabel; }

@Override
public String getCtaLink() { return ctaLink; }
}

I always try to first write down the interface before the implementation. This forces me to think about the public API and what parts of a possible implementation make sense to expose.

In HTL, reference the interface:

<div data-sly-use.hero="com.mysite.core.models.Hero">
<h1>${hero.heading}</h1>
</div>

Benefits:

  • Testability -- mock the interface in unit tests
  • Encapsulation -- hide implementation details
  • Swappability -- replace the implementation without changing HTL

Sling Model Exporters

Export your model as JSON (or other formats) for headless use cases:

@Model(
adaptables = Resource.class,
adapters = { Hero.class, ComponentExporter.class },
resourceType = "mysite/components/hero",
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = "jackson", extensions = "json")
public class HeroImpl implements Hero {
// ... same as above
}

The ComponentExporter interface (from com.adobe.cq.export.json) is what enables the .model.json export. Now you can access the component as JSON:

curl http://localhost:4502/content/mysite/en/jcr:content/root/container/hero.model.json

This is how AEM supports headless delivery of component data.

Testing Sling Models

Sling Models are testable with AemContext from io.wcm.testing.mock:

@ExtendWith(AemContextExtension.class)
class HeroImplTest {

private final AemContext context = new AemContext();

@Test
void testHeading() {
context.build()
.resource("/content/test", Map.of(
"sling:resourceType", "mysite/components/hero",
"heading", "Welcome",
"subheading", "To our site"
))
.commit();

Resource resource = context.resourceResolver().getResource("/content/test");
Hero hero = resource.adaptTo(Hero.class);

assertEquals("Welcome", hero.getHeading());
assertEquals("To our site", hero.getSubheading());
}
}

Troubleshooting -- when models do not adapt

SymptomWhat to inspectTypical fix
resource.adaptTo(MyModel.class) is null@Model adaptable + injection strategyMatch adaptable type, use OPTIONAL/defaults for authoring safety
Field is unexpectedly nullStored property key/path in JCRAlign @ValueMapValue(name=...) with actual property
Multifield collection is emptyChild node structure under component nodeVerify composite=true and @ChildResource target
@OSGiService not injectedOSGi component/service statusEnsure service is active and interface wiring is correct
Works in console edits but breaks on deploySource-controlled config/package contentsPersist changes in code (ui.config, ui.apps, core)

Service users and repoinit

When your code runs outside a request context (schedulers, event handlers, workflows) or needs elevated permissions, you cannot rely on the request-scoped resource resolver. Instead, you use a service user -- a dedicated system account with minimal permissions.

Why service users?

  • Never use admin sessions in application code -- this grants full access and is a security risk.
  • Service users follow the principle of least privilege -- only grant the permissions your service actually needs.
  • In AEMaaCS, CRXDE-based user creation is not available in production environments. Service users must be created via repoinit (Repository Initializer) scripts committed in your codebase.

Creating a service user with repoinit

Repoinit scripts run at bundle startup and set up repository state (users, groups, ACLs). Add them as OSGi configurations in ui.config:

// ui.config/.../config/org.apache.sling.jcr.repoinit.RepositoryInitializer-mysite.cfg.json
{
"scripts": [
"create service user mysite-reader\nset ACL for mysite-reader\n allow jcr:read on /content/mysite\nend"
]
}

Or, more readably, with a multi-line script:

create service user mysite-reader

set ACL for mysite-reader
allow jcr:read on /content/mysite
allow jcr:read on /content/dam/mysite
end

Mapping a service user

Map your bundle to the service user so getServiceResourceResolver knows which user to use. Add an amended service user mapping configuration:

// ui.config/.../config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-mysite.cfg.json
{
"user.mapping": [
"com.mysite.core:mysite-reader=mysite-reader"
]
}

The format is <bundle-symbolic-name>:<sub-service-name>=<service-user-name>.

Using the service user in code

@Component(service = ContentReaderService.class)
public class ContentReaderServiceImpl implements ContentReaderService {

@Reference
private ResourceResolverFactory resolverFactory;

public Resource readContent(String path) throws LoginException {
Map<String, Object> params = Map.of(
ResourceResolverFactory.SUBSERVICE, "mysite-reader"
);
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(params)) {
return resolver.getResource(path);
}
}
}

Important: Always close service resource resolvers (use try-with-resources). A leaked resolver is a resource leak.

Security and service access hints

  • Prefer the resolver already provided by AEM request/resource context.
  • For background jobs or elevated reads/writes, use service users with least privilege (getServiceResourceResolver).
  • Never use admin sessions in application code.
  • Avoid logging raw user input, tokens, or personally identifiable data.

For the full annotation reference, see the Sling Models and Services, Sling Model Annotations, and @ChildResource references.

Summary

You learned:

  • Sling Model basics -- @Model, @ValueMapValue, @Default
  • Adaptables -- Resource.class vs SlingHttpServletRequest.class
  • Injection annotations: @ValueMapValue, @ChildResource, @Self, @OSGiService, @ScriptVariable, @RequestAttribute, @ResourcePath
  • @PostConstruct for initialization logic
  • Optional vs required injection strategies
  • Interface-based models with adapters for clean APIs
  • Model Exporters for JSON output
  • Testing with AemContext
  • Service users and repoinit scripts for background/elevated access

With components (HTL + dialogs + Sling Models) covered, we are ready to build a complete site. The next chapter covers templates and policies -- how pages are structured and which components are allowed.

Next up: Templates & Policies -- editable templates, template types, structure vs initial content, component policies, and page structure.