> 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.

# Sling Model Exporter

## 17 – Sling Model Exporter

> **Target:** 3–4 years experienced AEM Developer **Syllabus point covered (26):** _"What is the Sling Model Exporter?"_ **Project domain:** a global energy technology company's marketing site.

***

## Before we start — where this fits

You now have three ways to get content out of AEM as JSON, and an interviewer who asks about the Model Exporter will very often follow up with _"so when would you use this rather than GraphQL?"_

That follow-up is the real question. Here is the answer in one line, and the rest of this file supports it:

> **The Sling Model Exporter gives you the page's structure as JSON. Content Fragments and GraphQL give you content that has no page structure at all.**

Which means they suit different consumers:

- A consumer that **mirrors the authored page** — an SPA where a React component corresponds to an AEM component — wants the **Model Exporter**.
- A consumer with **its own structure** — a native mobile app, a partner system — wants **Content Fragments and GraphQL** (file 15).

If you can make that distinction, you've answered the question properly rather than describing an annotation.

***

## 1. Introduction

### 1.1 What it is

> **The Sling Model Exporter turns a Sling Model into JSON, served at a URL, without you writing a servlet.**

You add one annotation to a model you already have:

```java
@Exporter(name = "jackson", extensions = "json")
```

And the model becomes available at:

```text
/content/energy/global/en/products/jcr:content/root/container/cta.model.json
```

**That's it.** No servlet, no serialisation code, no route registration.

### 1.2 The problem it solves

From file 07: when the browser needs _data_ rather than a page, you write a servlet. And a lot of those servlets do exactly the same thing — read a resource, build an object, serialise it as JSON.

You already have something that reads a resource and builds an object. It's the Sling Model. The Model Exporter removes the duplication.

**Without it:**

```java
@Component(service = Servlet.class)
@SlingServletResourceTypes(resourceTypes = "energy/components/cta",
                           selectors = "json", extensions = "json")
public class CtaJsonServlet extends SlingSafeMethodsServlet {
    @Override
    protected void doGet(...) {
        CtaModel model = request.getResource().adaptTo(CtaModel.class);
        response.setContentType("application/json");
        new ObjectMapper().writeValue(response.getWriter(), model);
    }
}
```

**With it:** one annotation on the model, and the servlet disappears.

### 1.3 A real project example to adapt

> "We use the Model Exporter in two places. The obvious one is a couple of components that expose their authored content as JSON for a front-end widget that loads asynchronously — the `.model.json` endpoint means we didn't have to write a servlet for each.
>
> The more interesting decision was where **not** to use it. Our mobile app doesn't consume `.model.json`, because that JSON mirrors our page structure — it's shaped like the page, with our component names and our nesting. The app has its own information architecture, so tying it to how we happen to lay out a web page would be a bad contract. That's Content Fragments and GraphQL instead.
>
> The thing we got wrong initially was treating the exported JSON as an implementation detail. It isn't — it's a public API contract. Renaming a getter renames a JSON field and breaks the consumer, so we use `@JsonProperty` to decouple the Java name from the JSON name."

That covers the use, the deliberate non-use with reasoning, and a genuine lesson — three follow-ups pre-empted.

***

## 2. Core Concepts

### 2.1 Making a model exportable

**Three things are required**, and the second one is where people get stuck.

```java
@Model(
        adaptables = SlingHttpServletRequest.class,
        adapters = { ComponentExporter.class, Cta.class },
        resourceType = "energy/components/cta",          // ← REQUIRED
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,   // "jackson"
        extensions = ExporterConstants.SLING_MODEL_EXTENSION  // "json"
)
public class CtaImpl implements Cta {
    // ...
}
```

**1. `@Exporter`** — this is what makes it exportable. `name = "jackson"` selects the Jackson serialiser; `extensions = "json"` says which extension it responds to. Using the `ExporterConstants` rather than string literals is the conventional form.

**2. `resourceType` on `@Model`** — **this is required and it's the usual cause of a 404.**

Think about how the request resolves (file 01): a request for `/content/.../cta.model.json` resolves the resource, reads its `sling:resourceType`, and then needs to find a model bound to that type with an exporter. Without `resourceType` on the `@Model`, there's no binding, so nothing is found.

**3. Adaptable from the request** — for the SPA Editor case specifically, and generally the safer choice for an exported model, because the exporter machinery works in a request context.

**And a fourth thing that isn't strictly required but matters:** `adapters` including the interface. That's the file 05 interface pattern, and here it also controls what gets exported.

### 2.2 The URL

```text
<resource path>.model.json
```

**On a component:**

```text
/content/energy/global/en/products/jcr:content/root/container/cta.model.json
```

**On a page:**

```text
/content/energy/global/en/products.model.json
```

**`model` is a selector and `json` is an extension**, which — from files 01 and 07 — means the URL is **entirely path-based** and therefore **dispatcher-cacheable**. That's a genuine benefit over a servlet taking query parameters.

**But it also means the dispatcher has to allow it.** The `model` selector and `json` extension need to be in the filter rules, or you get the same environment-specific 404 as every other path-allowlist problem in this repository.

### 2.3 What gets exported — and the security consequence

**By default, every public getter on the model is serialised.**

That is the single most important behavioural fact here, and it has a consequence people miss:

> **Adding a getter to an exported model changes your public API.**

A getter you added for internal use — a helper the HTL calls, a debugging convenience, a path you resolve — becomes a field in JSON that anyone can fetch.

**So `@JsonIgnore` is not optional hygiene; it's how you control the contract.**

```java
@JsonIgnore
public String getInternalPath() {
    return internalPath;    // used by HTL, NOT part of the JSON contract
}
```

**The interview point:**

> "The default is expose-everything — every public getter is serialised. That means adding a getter to an exported model silently changes the public API, which is easy to do without noticing. So `@JsonIgnore` on anything that isn't part of the contract, and I treat the exported JSON as an API rather than an implementation detail."

### 2.4 Controlling the output with Jackson annotations

| Annotation                 | Effect                             |
| -------------------------- | ---------------------------------- |
| `@JsonProperty("linkUrl")` | Rename the field in the JSON       |
| `@JsonIgnore`              | Exclude a getter entirely          |
| `@JsonInclude(NON_NULL)`   | Omit null values from the output   |
| `@JsonFormat`              | Control date and number formatting |
| `@JsonPropertyOrder`       | Fix the field order                |

**`@JsonProperty` deserves emphasis**, and this is the lesson from section 1.3:

The exported JSON is a **contract with a consumer**. If the JSON field name is derived from your Java getter name, then renaming `getLinkUrl()` to `getUrl()` — an ordinary refactor — silently renames a JSON field and breaks whoever is consuming it.

**`@JsonProperty` decouples them.** The Java name can change; the JSON name is stated explicitly and deliberately.

```java
@JsonProperty("url")
public String getResolvedLinkUrl() {   // rename this freely
    return resolvedUrl;                 // the JSON field stays "url"
}
```

**That's a genuinely good practice to volunteer**, because it shows you think about the consumer.

### 2.5 ComponentExporter and the SPA Editor

**This is where the Model Exporter gets architecturally interesting**, and it's the reason it exists in the form it does.

**The SPA Editor** lets you author a React or Angular single-page application inside AEM. The SPA fetches a JSON representation of the page and renders it, and AEM's editor overlays authoring UI on top.

**For that to work, the page has to be describable as JSON** — the whole component tree, nested, with each component identifying its type so the SPA knows which React component to render.

**Two interfaces make it work:**

| Interface           | Provides                                        | For                                |
| ------------------- | ----------------------------------------------- | ---------------------------------- |
| `ComponentExporter` | `getExportedType()`                             | **Every** exportable component     |
| `ContainerExporter` | `getExportedItems()`, `getExportedItemsOrder()` | Components that **contain** others |

**`getExportedType()` returns the resource type**, which is how the SPA maps a piece of JSON to a component:

```javascript
MapTo('energy/components/cta')(CtaComponent);
```

**The resulting JSON is page-shaped:**

```json
{
  ":type": "energy/components/page",
  ":items": {
    "root": {
      ":type": "wcm/foundation/components/responsivegrid",
      ":items": {
        "cta": {
          ":type": "energy/components/cta",
          "linkText": "Read the Press Release",
          "url": "/global/en/news/grid-upgrade.html"
        }
      },
      ":itemsOrder": ["cta"]
    }
  },
  ":itemsOrder": ["root"]
}
```

**The `:`-prefixed keys are structural metadata** — `:type`, `:items`, `:itemsOrder` — while the unprefixed ones are the component's own content. That separation is deliberate, so a consumer can distinguish structure from data.

**And notice `:itemsOrder`.** JSON objects have no guaranteed key order, so the order has to be stated explicitly. That's a small design detail worth noticing, and it explains why `ContainerExporter` has two methods rather than one.

### 2.6 Model Exporter versus GraphQL — the question that follows

**This is the comparison an interviewer will reach for**, and it's the important part of the topic.

|                   | **Sling Model Exporter**               | **Content Fragments + GraphQL**          |
| ----------------- | -------------------------------------- | ---------------------------------------- |
| What it exposes   | **A component's content, page-shaped** | **Structured content, page-independent** |
| Structure mirrors | Your **page and component tree**       | Your **content model**                   |
| Coupled to        | How the page is authored               | Nothing — fragments stand alone          |
| Consumer must     | Understand your component structure    | Understand your content model            |
| Best for          | **SPA Editor**, page-aware consumers   | **Native apps, partners**, true headless |
| Setup             | One annotation                         | Model design plus query setup            |
| Cacheable         | Yes — path-based GET                   | Yes — with persisted queries             |
| Query flexibility | None — you get what the model exposes  | The consumer selects fields              |

**The distinction to state:**

> "The Model Exporter gives you the **page's structure** as JSON. It's shaped like the page — component names, nesting, our layout decisions. That's exactly right for the SPA Editor, where a React component corresponds to an AEM component and the SPA is deliberately mirroring the authored page.
>
> It's the wrong contract for a native mobile app, because the app has its own information architecture. Tying it to how we happen to lay out a web page means our layout refactor breaks their app. That's Content Fragments and GraphQL — content that has no page structure at all, and the consumer picks the fields it needs.
>
> So the question I'd ask is: **does the consumer want to mirror our page, or does it have its own structure?**"

### 2.7 When to use a custom servlet instead

Worth having, because the honest answer isn't always "use the exporter."

**Use the Model Exporter** when the JSON you want is essentially "this component's content."

**Use a custom servlet** (file 07) when you need something the model doesn't naturally produce — aggregating several resources, applying a filter passed in the request, returning a rendered HTML fragment rather than JSON, or shaping the response for a specific consumer.

**The Load More endpoint from file 02 is the example.** It returns rendered HTML, takes a batch number as a suffix, and clamps it. None of that is a model exporting itself, so a servlet is correct.

***

## 3. Internal Working

### 3.1 How a.model.json request resolves

graph TD A\["GET /content/.../cta.model.json"] --> B\["Sling resolves the RESOURCE"] B --> C\["Reads sling:resourceType"] C --> D\{"Is there a model registered\<br/>for this resourceType\<br/>WITH an @Exporter?"} D -->|No| E\["Falls through to the\<br/>DefaultGetServlet — you get\<br/>raw node JSON, not your model"] D -->|Yes| F\["Instantiate the model\<br/>— injections, @PostConstruct"] F --> G\["Jackson serialises\<br/>every public getter"] G --> H\["JSON response"]

**The branch on the right is the diagnostic that matters.**

If no model is found, Sling doesn't error — it falls through to the **`DefaultGetServlet`** from file 07, which renders the node as raw JSON. So you get _some_ JSON back, just not yours.

**That's a genuinely confusing symptom**, because a naive check ("did I get JSON? yes") passes while the answer is wrong. The tell is that the output looks like raw node properties — `jcr:primaryType`, `sling:resourceType` — rather than your model's fields.

**And the usual cause is a missing `resourceType` on `@Model`.**

### 3.2 The full chain

Worth seeing because it ties several files together:

graph LR A\["Request\<br/>.model.json"] --> B\["Resource\<br/>file 01"] B --> C\["Resource type\<br/>file 02"] C --> D\["Sling Model\<br/>file 05"] D --> E\["@PostConstruct\<br/>runs once"] E --> F\["Jackson serialises\<br/>public getters"] F --> G\["Cacheable JSON\<br/>files 01, 07"]

**Note `@PostConstruct` still runs**, with everything from file 05 applying — expensive work happens once, and a model that returns null adapts to nothing.

**And the caching point:** because it's a selector-and-extension URL, it's path-based and the dispatcher caches it as a file. Same principle as everywhere else in this repository.

***

## 8. Coding Examples

### 8.1 An exported model, annotated

```java
package com.energy.core.models.impl;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.energy.core.models.Cta;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct;

@Model(
        // Request-adaptable: the exporter works in a request context,
        // and this is what the SPA Editor machinery expects.
        adaptables = SlingHttpServletRequest.class,

        // Register as the interface AND as ComponentExporter, which is
        // what lets the SPA Editor treat this as a component.
        adapters = { Cta.class, ComponentExporter.class },

        // REQUIRED for export.
        //
        // Resolution goes: URL → resource → resource type → model.
        // Without this binding, Sling finds no model and falls through
        // to the DefaultGetServlet -- which returns the RAW NODE as
        // JSON. You get a 200 and valid JSON, just not yours.
        resourceType = CtaImpl.RESOURCE_TYPE,

        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,      // "jackson"
        extensions = ExporterConstants.SLING_MODEL_EXTENSION     // "json"
)
// Omit nulls rather than emitting "field": null for everything unset
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CtaImpl implements Cta, ComponentExporter {

    protected static final String RESOURCE_TYPE = "energy/components/cta";

    @ValueMapValue
    private String linkText;

    @ValueMapValue
    private String linkUrl;

    @SlingObject
    private ResourceResolver resourceResolver;

    private String resolvedUrl;

    @PostConstruct
    protected void init() {
        // Same rule as file 05: do the work ONCE. Serialisation calls
        // every exported getter, so an expensive getter costs on every
        // JSON request as well as every HTL call.
        this.resolvedUrl = resolveUrl(linkUrl);
    }

    // ---------------------------------------------------------------
    // EXPORTED FIELDS
    //
    // @JsonProperty names the JSON field EXPLICITLY, decoupling it from
    // the Java method name. Without this, renaming a getter -- an
    // ordinary refactor -- silently renames a JSON field and breaks the
    // consumer. We shipped exactly that once.
    // ---------------------------------------------------------------

    @Override
    @JsonProperty("text")
    public String getLinkText() {
        return StringUtils.defaultString(linkText);
    }

    @Override
    @JsonProperty("url")
    public String getLinkUrl() {          // rename this freely -- JSON stays "url"
        return resolvedUrl;
    }

    // ---------------------------------------------------------------
    // NOT EXPORTED
    //
    // The default is EXPOSE EVERYTHING -- every public getter becomes a
    // JSON field. So a helper the HTL needs silently becomes public
    // data. @JsonIgnore is how you control the contract, and it also
    // means the getter isn't invoked during serialisation.
    // ---------------------------------------------------------------

    @Override
    @JsonIgnore
    public boolean isReady() {
        return StringUtils.isNotBlank(linkText) && StringUtils.isNotBlank(resolvedUrl);
    }

    @Override
    @JsonIgnore
    public String getStyle() {
        return "arrow";      // presentation -- not part of the data contract
    }

    // ---------------------------------------------------------------
    // ComponentExporter: how a SPA maps this JSON to a component.
    // The SPA does MapTo('energy/components/cta')(CtaComponent).
    // ---------------------------------------------------------------

    @Override
    @JsonProperty(":type")
    public String getExportedType() {
        return RESOURCE_TYPE;
    }

    private String resolveUrl(String url) {
        if (StringUtils.isBlank(url)) {
            return null;
        }
        return url.startsWith("/content")
                ? resourceResolver.map(url) + ".html"
                : url;
    }
}
```

**The five decisions to defend:**

**`resourceType` is present** — and the comment explains the failure mode if it isn't.

**`@JsonProperty` on every exported field**, decoupling the JSON contract from Java names.

**`@JsonIgnore` on internal getters**, deliberately, because the default is expose-everything.

**`@JsonInclude(NON_NULL)`** so the output isn't cluttered with nulls.

**`getExportedType()` returns the resource type**, which is what the SPA maps against.

### 8.2 The resulting JSON

```text
GET /content/energy/global/en/products/jcr:content/root/container/cta.model.json
```

```json
{
  ":type": "energy/components/cta",
  "text": "Read the Press Release",
  "url": "/global/en/news/grid-upgrade.html"
}
```

**Note what's absent:** `isReady` and `style` aren't there, because they're `@JsonIgnore` d. And `resolvedLinkUrl` isn't there — it's `url`, because `@JsonProperty` said so.

### 8.3 A container, for the SPA Editor

```java
@Model(
        adaptables = SlingHttpServletRequest.class,
        adapters = { Container.class, ContainerExporter.class, ComponentExporter.class },
        resourceType = ContainerImpl.RESOURCE_TYPE,
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
          extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ContainerImpl implements Container, ContainerExporter {

    protected static final String RESOURCE_TYPE = "energy/components/container";

    @SlingObject
    private Resource resource;

    private Map<String, ComponentExporter> exportedItems;
    private String[] exportedItemsOrder;

    @PostConstruct
    protected void init() {
        // Build ONCE. And BOUND it -- the same rule as file 05.
        // A container with an unexpected number of children shouldn't
        // be able to produce an enormous serialisation.
        this.exportedItems = buildItems();
        this.exportedItemsOrder = exportedItems.keySet().toArray(new String[0]);
    }

    /** Child components, keyed by node name. */
    @Override
    @JsonProperty(":items")
    public Map<String, ? extends ComponentExporter> getExportedItems() {
        return exportedItems;
    }

    /**
     * The order is a SEPARATE field because JSON objects have no
     * guaranteed key order. A consumer can't rely on the order of
     * ":items", so the order has to be stated explicitly.
     */
    @Override
    @JsonProperty(":itemsOrder")
    public String[] getExportedItemsOrder() {
        return exportedItemsOrder.clone();      // defensive copy
    }

    @Override
    @JsonProperty(":type")
    public String getExportedType() {
        return RESOURCE_TYPE;
    }

    private Map<String, ComponentExporter> buildItems() {
        Map<String, ComponentExporter> items = new LinkedHashMap<>();
        int count = 0;
        for (Resource child : resource.getChildren()) {
            if (count++ >= MAX_ITEMS) {         // BOUND IT
                break;
            }
            // adaptTo can ALWAYS return null -- guard it (file 05)
            ComponentExporter exporter = child.adaptTo(ComponentExporter.class);
            if (exporter != null) {
                items.put(child.getName(), exporter);
            }
        }
        return items;
    }
}
```

**The `:itemsOrder` comment is the detail worth knowing** — it explains why `ContainerExporter` has two methods rather than one, and it's a small design point that shows you've read the interface rather than just used it.

### 8.4 The dispatcher rule

```text
# .model.json is a selector-and-extension URL, so it's path-based and
# CACHEABLE -- a real advantage over a servlet taking query parameters.
#
# But the dispatcher denies by default, so it has to be allowed
# explicitly. Otherwise it works on author and 404s on publish -- the
# same environment-specific failure as allowProxy and path-bound
# servlets elsewhere in this repository.
/0110 { /type "allow" /selectors "model" /extension "json" /path "/content/*" }
```

### 8.5 What the SPA does with it

```javascript
// The resource type from ":type" binds the JSON to a React component.
import { MapTo } from '@adobe/aem-react-editable-components';

const Cta = ({ text, url }) => (
    <a className="cmp-cta" href={url}>{text}</a>
);

MapTo('energy/components/cta')(Cta);
```

**That's the payoff of `getExportedType()`** — the SPA doesn't need to know anything about our page except the resource types, and AEM's editor can overlay authoring UI because the mapping is explicit.

***

## 9. Common Mistakes

| The mistake                             | What happens                                                         | The fix                                           |
| --------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------- |
| No `resourceType` on `@Model`           | **Falls through to the DefaultGetServlet** — valid JSON, wrong shape | Add `resourceType`                                |
| Not noticing the wrong shape            | A 200 and valid JSON, so a naive check passes                        | Look for `jcr:primaryType` in the output          |
| Relying on getter names for JSON fields | **A refactor silently breaks the consumer**                          | `@JsonProperty` on every exported field           |
| Not using `@JsonIgnore`                 | Helper getters become public API                                     | Ignore anything not in the contract, deliberately |
| Treating the JSON as internal           | It's a published API with unknown consumers                          | Review exported models more carefully             |
| Dispatcher not allowing the selector    | Works on author, 404 on publish                                      | Allow `model` selector, `json` extension          |
| Expensive getters exported              | Executed on every JSON request, not just HTL                         | `@JsonIgnore`, or move work to `@PostConstruct`   |
| Unbounded container export              | An enormous serialisation                                            | Bound it, as everywhere                           |
| Using `.model.json` for a native app    | **Couples them to your page layout**                                 | Content Fragments + GraphQL                       |
| Not planning invalidation               | Cached JSON goes stale                                               | Flush on content publish                          |
| Exporting sensitive data                | It's as public as the resource                                       | `@JsonIgnore`, and review what's exposed          |

***

## 10. Best Practices

**Always set `resourceType`.** Without it the export silently doesn't work, in a way that looks like it does.

**Treat the JSON as a published API.** `@JsonProperty` on every exported field so Java names can change freely. `@JsonIgnore` on everything not in the contract. Remember you can add fields safely but not rename or remove them.

**Default to hiding.** The framework's default is expose-everything, so the discipline has to come from you.

**Keep getters cheap**, because serialisation calls all of them. Work belongs in `@PostConstruct`.

**Bound anything that builds a collection**, especially container exports.

**Allow the selector in the dispatcher**, and plan invalidation for the cached JSON.

**Choose deliberately between this and GraphQL.** Page-shaped for a page-mirroring consumer; Content Fragments for anything with its own structure.

***

## 11. Debugging Tips

**"`.model.json` returns JSON but not my fields."** Look at the output. If it contains `jcr:primaryType` and `sling:resourceType`, you're seeing the **DefaultGetServlet** rendering the raw node — Sling found no model for that resource type. The cause is almost always a missing `resourceType` on `@Model`.

**"It returns 404."** Either the resource doesn't exist at that path, or — on publish — the dispatcher isn't allowing the `model` selector. Check `dispatcher.log` to see whether the request reached AEM at all, the same split as everywhere else.

**"A field is missing."** Either `@JsonIgnore` is on it, or the getter returned null and `@JsonInclude(NON_NULL)` omitted it. Both are easy to forget you configured.

**"A field appeared that shouldn't be there."** Someone added a public getter. That's the expose-everything default doing what it does.

**"Jackson can't serialise something."** A getter returns a type Jackson doesn't know how to handle — a `Resource`, a `Page`, a JCR type. Return simple types or a small DTO instead.

| Tool                              | Answers                                  |
| --------------------------------- | ---------------------------------------- |
| The JSON output's shape           | Your model, or the DefaultGetServlet     |
| `/system/console/status-adapters` | Is the model registered at all (file 05) |
| `/system/console/servletresolver` | What handles this URL                    |
| `dispatcher.log`                  | Did the request reach AEM                |
| `error.log`                       | Jackson serialisation failures           |

***

## 12. Performance Notes

**Serialisation calls every exported getter**, so an expensive getter now costs on every JSON request as well as every HTL call. `@JsonIgnore` removes both the field and the call.

**All of file 05 applies** — `@PostConstruct` for the work, cheap getters, bounded collections.

**Caching is the big lever.** `.model.json` is a path-based URL, so the dispatcher serves it as a file and most requests never reach publish. That's a genuine advantage over a servlet taking query parameters.

**Container exports can fan out.** A container exporting children which export their children means a deep tree serialised in one request. Bound it.

***

## 14. Comparison Tables

**Model Exporter vs Content Fragments + GraphQL vs custom servlet**

|                         | Model Exporter      | CF + GraphQL              | Custom servlet             |
| ----------------------- | ------------------- | ------------------------- | -------------------------- |
| Shape                   | **Page-shaped**     | **Content-shaped**        | Whatever you write         |
| Coupled to              | Your component tree | Nothing                   | Your design                |
| Effort                  | One annotation      | Model + queries           | A full servlet             |
| Consumer selects fields | No                  | **Yes**                   | Depends                    |
| Cacheable               | Yes (selector)      | Yes (persisted)           | Depends                    |
| Best for                | **SPA Editor**      | **Native apps, partners** | Aggregation, HTML, bespoke |

**The exporter interfaces**

| Interface           | Method                                    | Purpose                              |
| ------------------- | ----------------------------------------- | ------------------------------------ |
| `ComponentExporter` | `getExportedType()` → `:type`             | Which component this is              |
| `ContainerExporter` | `getExportedItems()` → `:items`           | Children                             |
|                     | `getExportedItemsOrder()` → `:itemsOrder` | **Order — JSON keys aren't ordered** |

**Jackson annotations**

| Annotation               | Effect                                                         |
| ------------------------ | -------------------------------------------------------------- |
| `@JsonProperty("x")`     | **Name the field explicitly** — decouples from the getter name |
| `@JsonIgnore`            | Exclude, and skip the getter call                              |
| `@JsonInclude(NON_NULL)` | Omit nulls                                                     |
| `@JsonFormat`            | Date and number formatting                                     |

***

## Next topic

**`18-Replication-and-Distribution`** — your syllabus point 27, and the last of the core set: what replication is, how agents work on 6.5, how Sling Content Distribution replaces them on Cloud Service, reverse replication, flush agents, and the queue problems that show up in production.

***

_Topic 17 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain._
