AEM Annotations: The Complete Reference (OSGi, Sling Models, Servlets)
Every annotation an AEM developer uses — OSGi Declarative Services (@Component, @Reference, @Activate), Metatype config (@ObjectClassDefinition, @AttributeDefinition), Sling Models injectors (@ValueMapValue, @ChildResource, @Self, @Via), servlet annotations, and Jackson exporters — with examples and a cheat-sheet table.

Modern AEM development is almost entirely annotation-driven. You rarely write boilerplate wiring code by hand; instead you declare what you want with annotations, and the runtime — OSGi for services, Sling Models for content adaptation, the Sling engine for servlets — does the wiring for you. That's powerful, but it also means a working AEM developer needs to keep a few dozen annotations straight, and it's genuinely easy to forget which package an annotation comes from or which of two similar ones to reach for.
This article is that reference. It's organized by the three families you'll use every day — OSGi Declarative Services, Sling Models, and Sling Servlets — and for each annotation it explains not just the syntax but why and when you use it, with a runnable example. A consolidated cheat-sheet table sits at the end so you can look anything up at a glance.
It pairs naturally with the Component Development guide, which shows these annotations working together, and the AEM Developer Cheat Sheet for the surrounding architecture.
OSGi Declarative Services (DS R6/R7)
Package: org.osgi.service.component.annotations
OSGi is the module system your Java runs inside, and Declarative Services (DS) is how you register components and connect them together. These annotations are the foundation of every service, servlet, and scheduled job you'll write.
One thing to settle up front: DS is the modern, correct standard. You may still encounter the old Felix SCR annotations (@Service, @Property, and a different @Reference from org.apache.felix.scr.annotations) in legacy code, but those are deprecated — new code should always use the OSGi DS annotations below.
@Component
@Component is the starting point: it tells OSGi that a class is a managed component, and optionally that it provides a service other code can consume.
@Component(
service = MyService.class, // the interface(s) it provides
immediate = true, // activate without waiting for a consumer
property = { // service properties
"process.label=My Process"
})
public class MyServiceImpl implements MyService { }
The service attribute publishes the class under an interface so others can inject it. immediate = true starts the component as soon as its dependencies are satisfied, rather than lazily on first use — important for things like listeners that need to be running immediately. The property array attaches metadata that other parts of OSGi (event handlers, schedulers, servlet registration) read.
@Activate, @Deactivate, @Modified
These three mark lifecycle callbacks, so your component can react to being started, reconfigured, or stopped.
@Activate
@Modified
protected void activate(Config config) { this.endpoint = config.endpoint(); }
@Deactivate
protected void deactivate() { /* cleanup */ }
@Activate runs when the component starts and is where you read configuration and set up state. @Modified runs when the configuration changes — annotating the same method with both means a config change is applied in place, without OSGi tearing the component down and recreating it. @Deactivate runs on shutdown and is where you release resources.
@Reference
@Reference injects another OSGi service into your component. It also lets you express how that dependency behaves — whether it's required, and whether it can come and go at runtime.
@Reference
private QueryBuilder queryBuilder;
// Optional + dynamic (allows the service to come and go)
@Reference(cardinality = ReferenceCardinality.OPTIONAL,
policy = ReferencePolicy.DYNAMIC)
private volatile SomeService optionalService;
// Multiple
@Reference
private List<ContentHandler> handlers;
A plain @Reference is a mandatory, static dependency — your component won't start until it's available. Setting cardinality to OPTIONAL and policy to DYNAMIC makes the dependency optional and allows it to appear or disappear while your component keeps running (note the volatile field). And injecting a List collects every service registered under that interface — the basis of a plugin pattern.
@Designate
@Designate is the link between a component and its configuration definition. Without it, the configuration class in the next section never binds to the component.
@Component(service = MyService.class)
@Designate(ocd = MyServiceImpl.Config.class)
public class MyServiceImpl implements MyService { }
The ocd ("object class definition") attribute points at the @ObjectClassDefinition-annotated interface that describes your configurable properties.
OSGi Metatype (configuration)
Package: org.osgi.service.metatype.annotations
These annotations describe a component's configurable properties — the ones that appear, editable, in the OSGi Configuration Manager console and can be supplied as configuration in your project's ui.config module.
@ObjectClassDefinition & @AttributeDefinition
You define configuration as an annotated @interface. Each method becomes one editable property, and @AttributeDefinition gives it a label, a type (inferred from the return type), and a default.
@ObjectClassDefinition(name = "My Service Configuration",
description = "Settings for My Service")
public @interface Config {
@AttributeDefinition(name = "API endpoint", description = "Base URL")
String endpoint() default "https://api.example.com";
@AttributeDefinition(name = "Timeout (ms)")
int timeout() default 5000;
@AttributeDefinition(name = "Enabled paths",
cardinality = 50) // array → multi-value
String[] paths() default {"/content"};
@AttributeDefinition(name = "Mode",
options = {
@Option(label = "Fast", value = "fast"),
@Option(label = "Safe", value = "safe")
})
String mode() default "safe";
}
A few patterns are worth noting: an array return type with a cardinality produces a multi-value field, and an options list turns a field into a dropdown of fixed choices. You read these values back in your @Activate(Config config) method.
Gotcha: OSGi property names can't contain dots, but configuration keys often need them. The convention is that a double underscore in a method name maps to a dot in the property name — so
my__property()becomes the configuration keymy.property.
Sling Models
Package: org.apache.sling.models.annotations (plus the injectorspecific subpackage)
Sling Models are how you turn content into typed Java objects. A model adapts a Resource or a SlingHttpServletRequest into a bean whose fields are populated automatically from JCR properties, child nodes, services, and request data — so your templates and servlets work with clean getters instead of raw repository APIs.
@Model
@Model declares a class as a Sling Model and, crucially, says what it can be adapted from.
@Model(
adaptables = {Resource.class, SlingHttpServletRequest.class},
adapters = {Teaser.class, ComponentExporter.class}, // optional
resourceType = "mysite/components/teaser", // for exporters
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class TeaserModel implements Teaser { }
adaptables lists the source objects; adapters (optional) registers the model under one or more interfaces, which is how exporters and the SPA framework find it; and resourceType ties the model to a component so exporters resolve automatically.
Always set
defaultInjectionStrategy = OPTIONAL. The default is REQUIRED, which throws an exception if any field can't be injected — and in practice optional dialog fields are empty all the time. OPTIONAL leaves them null instead, which is what you want.
Injector-specific annotations (preferred)
The single most useful habit with Sling Models is to use injector-specific annotations rather than the generic @Inject. Each one names exactly where a value comes from, which makes the code self-documenting and avoids the ambiguity of letting Sling guess the source.
| Annotation | Injects from |
|---|---|
@ValueMapValue | A property on the resource's ValueMap |
@ChildResource | A child resource (single or List) |
@RequestAttribute | A request attribute |
@OSGiService | An OSGi service |
@ScriptVariable | An HTL/JSP binding (currentPage, pageManager, resource…) |
@Self | The adaptable itself (the request/resource) |
@SlingObject | Sling objects (ResourceResolver, SlingHttpServletRequest, Resource…) |
@ResourcePath | A resource at a configured/looked-up path |
In practice they read very clearly — the annotation tells you the origin of every field:
@ValueMapValue(name = "jcr:title") // map to a differently-named property
private String title;
@ValueMapValue
@Default(values = "Untitled")
private String heading;
@ChildResource
private List<Resource> items;
@ScriptVariable
private Page currentPage;
@SlingObject
private ResourceResolver resourceResolver;
@OSGiService
private QueryBuilder queryBuilder;
@Self
private SlingHttpServletRequest request;
Generic @Inject + modifiers
The older, generic injection style still works, and you'll see it in existing codebases. It relies on Sling choosing an injector by field name and type, and uses a set of modifier annotations to refine behavior. Prefer the injector-specific annotations above for new code, but it's worth recognizing these:
| Annotation | Purpose |
|---|---|
@Inject | Generic injection (Sling picks the injector by name/type) |
@Optional | Field is optional (with REQUIRED default strategy) |
@Default | Fallback value(s) when nothing is injected |
@Named | Inject by a specific name |
@Via | Adapt via an intermediate (e.g. delegate to super-type model) |
@Source | Force a specific injector |
@Filter | LDAP filter for @OSGiService selection |
@Via — delegating to a Core Component
@Via deserves a closer look because it powers one of the most common patterns in real projects: extending a Core Component's model and overriding just one piece of its behavior. You inject the original model and delegate to it, customizing only what you need.
@Self
@Via(type = ResourceSuperType.class)
private Teaser delegate; // the Core Component's Teaser model
public String getTitle() {
return StringUtils.upperCase(delegate.getTitle());
}
Here @Via(type = ResourceSuperType.class) tells Sling to adapt using the component's sling:resourceSuperType — i.e. the Core Component — so delegate is Adobe's fully-featured Teaser model, and you simply wrap the one method you care about.
@PostConstruct
Package: javax.annotation (this import trips people up — it is not a Sling annotation).
A method annotated with @PostConstruct runs once, after all injection has completed. It's the correct place for any derived or computed values, because by then every injected field is guaranteed to be populated.
@PostConstruct
protected void init() {
this.formatted = title != null ? title.trim() : "";
}
Exporters: @Exporter / @Exporters
When a component needs to be consumed as JSON — by the SPA Editor or a headless front end — you don't write a serializer. You add @Exporter, and Sling serializes the model with Jackson.
@Model(adaptables = SlingHttpServletRequest.class,
adapters = {Teaser.class, ComponentExporter.class},
resourceType = "mysite/components/teaser",
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = "jackson", extension = "json")
public class TeaserModel implements Teaser, ComponentExporter { }
You then shape the JSON output with standard Jackson annotations from com.fasterxml.jackson.annotation:
@JsonProperty("titleText") // rename
private String title;
@JsonIgnore // exclude
private String internalNote;
@JsonInclude(JsonInclude.Include.NON_NULL) // on the class/field
public class TeaserModel { }
@JsonProperty renames a field in the output, @JsonIgnore hides it entirely, and @JsonInclude can suppress nulls so the JSON stays lean.
Sling Servlet annotations
Package: org.apache.sling.servlets.annotations
These let you register servlets declaratively, which is far clearer than the old approach of stuffing registration details into @Component properties.
@SlingServletResourceTypes (preferred)
The recommended way to register a servlet is to bind it to a resource type. Doing so means the servlet inherits AEM's resource-level access control automatically.
@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "mysite/components/teaser",
selectors = "data",
extensions = "json",
methods = HttpConstants.METHOD_GET)
public class TeaserServlet extends SlingSafeMethodsServlet { }
This servlet responds to a GET for the teaser resource with the data selector and json extension — for example /content/.../teaser.data.json.
@SlingServletPaths
You can also bind a servlet to a fixed path, but do so sparingly: a path-bound servlet sidesteps resource-level access control and must be explicitly allowed through the Dispatcher.
@Component(service = Servlet.class)
@SlingServletPaths("/bin/mysite/export")
public class ExportServlet extends SlingAllMethodsServlet { }
@SlingServletFilter
To intercept requests before they reach a servlet — for logging, headers, or rewriting — register a filter with a scope and a URL pattern.
@Component(service = Filter.class)
@SlingServletFilter(
scope = {SlingServletFilterScope.REQUEST},
pattern = "/content/mysite/.*",
methods = {"GET"})
public class MyFilter implements Filter { }
Quick reference / cheat sheet
| Annotation | Package (short) | Purpose |
|---|---|---|
@Component | osgi.service.component | Declare OSGi component/service |
@Activate / @Deactivate / @Modified | osgi.service.component | Lifecycle callbacks |
@Reference | osgi.service.component | Inject another service |
@Designate | osgi.service.component | Bind config OCD |
@ObjectClassDefinition | osgi.service.metatype | Config definition |
@AttributeDefinition | osgi.service.metatype | Config property |
@Model | sling.models.annotations | Declare Sling Model |
@ValueMapValue | …injectorspecific | JCR property |
@ChildResource | …injectorspecific | Child node(s) |
@ScriptVariable | …injectorspecific | HTL global binding |
@Self | …injectorspecific | The adaptable |
@SlingObject | …injectorspecific | Sling runtime objects |
@OSGiService | …injectorspecific | Inject service into a model |
@Via | sling.models.annotations | Delegate / intermediate adapt |
@Default | sling.models.annotations | Fallback value |
@PostConstruct | javax.annotation | Post-injection logic |
@Exporter | sling.models.annotations | JSON serialization |
@JsonProperty / @JsonIgnore | jackson.annotation | Shape exporter output |
@SlingServletResourceTypes | sling.servlets.annotations | Resource-bound servlet |
@SlingServletPaths | sling.servlets.annotations | Path-bound servlet |
@SlingServletFilter | sling.servlets.annotations | Request filter |
Best practices & gotchas
- ✅ Use OSGi DS annotations, never the deprecated Felix SCR ones.
- ✅ Prefer injector-specific annotations (
@ValueMapValue,@ChildResource) over the generic@Inject. - ✅ Set
defaultInjectionStrategy = OPTIONALon every model. - ✅ Annotate one method with both
@Activateand@Modifiedso configuration changes apply without a restart. - ✅ Adapt from
SlingHttpServletRequest(notResource) when you need request-scoped data or exporters. - ❌ Don't mix Felix SCR and OSGi DS annotations in the same class.
- ❌ Don't forget
@Designate(ocd = ...)— without it your@ObjectClassDefinitionis never bound. - ❌ Don't import
@PostConstructfrom the wrong place — it lives injavax.annotation, not Sling.
Wrapping up
Annotations are the wiring diagram of AEM. Once you can place any annotation into one of the three families — OSGi DS for components, services, and configuration; Sling Models for adapting content into Java; and Sling Servlets for HTTP endpoints — the platform stops feeling like magic and starts feeling declarative and predictable. You describe intent; the runtime connects the dots.
To see all of these working together in a real component, read the Component Development guide, and for the templating layer continue to the HTL cheat sheet. To skip the boilerplate, generate fully annotated models with my AEM Component Generator.
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.