For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/annotations/child-resource.md.
The @ChildResource annotation is a Sling Models injector that maps child JCR nodes to Java objects. It is the standard way to read multifield dialog entries, nested configuration structures, or any child node hierarchy into your Sling Model.
Use @ChildResource whenever your component stores data as child nodes rather than simple properties on the component node itself. The most common scenario is a composite multifield dialog, where each entry the author adds becomes a child node in the JCR.
Scenario
Annotation
Storage
Single text value (./title)
@ValueMapValue
Property on the component node
Multi-value property (./tags)
@ValueMapValue with String[]
Multi-value property on the component node
Multifield with composite=true
@ChildResource
Each entry is a child node
Non-composite multifield
@ValueMapValue with String[]
Multi-value property (one per field)
[!-success] -success
tip
Always use composite="{Boolean}true" on your multifield dialogs. It stores entries as structured child nodes, which makes them easy to inject with @ChildResource. Without it, all values are stored as flat multi-value properties on the same node, which is harder to work with and doesn't support multiple fields per entry.
When an author fills in a composite multifield, AEM stores each entry as a separate child node under the component's jcr:content node. @ChildResource tells Sling Models to look for those child nodes and adapt each one to the target type.
Stores each entry as a child node (required for @ChildResource)
name="./products"
The JCR child node name under the component node
granite:data/min-items
Optional minimum number of entries
granite:data/max-items
Optional maximum number of entries
[!-warning] -warning
warning
If you forget composite="{Boolean}true", the multifield stores values as flat multi-value properties. @ChildResource will not work, and you will get null injected at runtime.
The child model must use adaptables = Resource.class. @ChildResource adapts each child node as a Resource, not as a SlingHttpServletRequest. If you use SlingHttpServletRequest.class, the injection will silently return null.
@ChildResource also works for a single child node, not just multifields:
// Reads the "configuration" child node directly@ChildResourceprivate Resource configuration;// Or adapt to a specific model@ChildResourceprivate HeaderConfig headerConfig;
This is useful for design dialogs or template policies that store structured data as a single child node.
Control what happens when the child node doesn't exist:
// Uses the model's defaultInjectionStrategy (OPTIONAL in our case)@ChildResourceprivate List<Product> products;// Explicitly optional - returns null if "products" node is missing@ChildResource(injectionStrategy = InjectionStrategy.OPTIONAL)private List<Product> products;// Required - model adaptation fails if "products" node is missing@ChildResource(injectionStrategy = InjectionStrategy.REQUIRED)private List<Product> products;
[!-success] -success
tip
Prefer setting defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL on the @Model annotation and only override with REQUIRED on fields that truly must exist. This prevents the entire model from returning null when a single optional field is missing.
Child models can themselves contain @ChildResource fields for deeply nested structures:
@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)public class Section { @ValueMapValue private String sectionTitle; // Each section has its own list of items @ChildResource private List<SectionItem> items;}
When a multifield lives inside a dialog tab, the JCR structure stays the same - the multifield's name attribute determines where the child nodes are stored, regardless of the dialog layout structure:
<!-- Tab structure in dialog (layout only - doesn't affect storage) --><tabs sling:resourceType="granite/ui/components/coral/foundation/tabs"> <items> <productTab jcr:title="Products"> <items> <!-- The multifield still writes to ./products --> <products sling:resourceType=".../multifield" composite="{Boolean}true"> <field name="./products"> <!-- ... fields ... --> </field> </products> </items> </productTab> </items></tabs>
#Using @Via for child resources on a different path
If the child data lives under a different resource than the component node (e.g. the jcr:content child of a referenced page), combine @ChildResource with a @Via annotation or resolve manually:
// Read child resources from a specific sub-path@ChildResource(name = "jcr:content/items")private List<Resource> pageItems;
Without composite="{Boolean}true", the multifield stores values as multi-value properties. This breaks @ChildResource injection and limits you to a single field per entry.
The child model class must declare adaptables = Resource.class. The @ChildResource injector passes a Resource to adaptTo(), not a SlingHttpServletRequest.
// Correct - child model adapts from Resource@Model(adaptables = Resource.class)public class Product { ... }// Wrong - @ChildResource cannot adapt a request@Model(adaptables = SlingHttpServletRequest.class)public class Product { ... }
Deeply nested multifields (multifield within a multifield) create complex JCR structures that are hard to query and maintain. Prefer flat structures where possible, or consider Content Fragments for highly structured data.
@ChildResource loads all child nodes eagerly. For components with a very large number of entries (50+), consider lazy loading or pagination. In practice, most multifield components have fewer than 20 entries, so this is rarely an issue.