SfSkills Domains Docs GitHub

Home / apex / apex-wrapper-class-patterns

apex-wrapper-class-patterns

Use when designing wrapper or inner classes in Apex to combine SObjects with computed fields, shape data for LWC consumption, or sort collections with Comparable or Comparator. Trigger keywords: wrapper class, inner class, Comparable, Comparator, @AuraEnabled fields, @JsonAccess, return a wrapper list to LWC, sort a list of wrapper objects. NOT for JSON serialization mechanics — use apex/apex-json-serialization. NOT for LWC data-binding patterns — use lwc/lwc-reactive-state-patterns.

Open the skill folder on GitHub


Apex Wrapper Class Patterns

Use this skill to design a wrapper class that combines sObject data with computed values, shapes results for a Lightning web component, an Aura component, or an Apex REST consumer, or sorts a list of custom objects. It covers where the wrapper class lives, which annotations each consumer needs, sharing behavior, and null-safe sorting.


Before Starting


Questions to Ask Before Configuring

QuestionWhy it mattersWhat a good answer addsWhat proper configuration adds over just doing it
"Will a Lightning web component or Aura component receive or send this object?"The LWC and Aura guides say an Apex inner class as a parameter or return value isn't supportedA top-level wrapper class instead of an inner classA component contract that is supported, not one that works until it doesn't
"Which properties does the component read?"Only public instance properties and methods annotated with @AuraEnabled are serializedThe exact list of annotated propertiesSmaller payloads and no undefined values in the template
"Is the wrapper also passed back to Apex as a parameter?"Custom-class parameters need @AuraEnabled on each property plus a getter and setter{get; set;} on those propertiesRound-trips that deserialize cleanly
"Does any wrapper method query or write records?"Inner classes don't adopt the container's sharing mode; in API 67.0 and later a class without a declaration runs with sharingAn explicit sharing keyword on the class that touches dataRecord access that is decided on purpose
"Can a sort key be null, or can the list contain null entries?"The Comparable and Comparator references say implementations must handle nulls to avoid a null pointer exceptionA null ordering rule (nulls first or last)Sorts that never fail on real data
"Does code in another namespace or package serialize this class?"From API 49.0 the default @JsonAccess for both directions is sameNamespace@JsonAccess only where cross-namespace access is realNo over-broad always access on internal types

What a proper design adds over "just add an inner class": the wrapper uses a supported component contract, exposes only what the client needs, sorts safely, and has a sharing and serialization posture someone chose.


Core Concepts

Where the class lives

ConsumerClass placementAnnotations
Lightning web component or Aura componentTop-level class (not an inner class, no inheritance)@AuraEnabled on each public property the client reads; getters and setters on properties received as parameters
Apex REST resource in the same namespaceTop-level or inner classNone required; @JsonAccess only to open access to another namespace or package
Internal Apex onlyInner class is fineNone

Inner classes behave like static Java inner classes: they can have instance member variables, but there is no implicit pointer to an instance of the outer class. Inner classes have no static methods or variables.

Two meanings of @AuraEnabled

On a static method it makes the method callable from a component. On instance properties and methods it makes them serializable when an instance is returned. The Aura guide says not to mix the two uses in the same Apex class, so keep the controller (static methods) and the wrapper (instance properties) in separate top-level classes.

Sharing

You can declare a sharing mode on inner and outer classes, and inner classes don't adopt the container's mode. In API 67.0 and later, a class without an explicit declaration runs with sharing. In 66.0 and earlier, such a class that isn't an entry point takes the sharing mode of its caller. Wrappers that only hold values never reach this question; keep data access in the service or controller.

Sorting

Comparable.compareTo(Object) gives one natural order for List.sort(). Comparator<T>.compare(T, T) is passed to List.sort(comparator) to choose an order at the call site. Both references require explicit null handling. For locale-sensitive order use Collator, and avoid it in triggers because results depend on the running user. UNVERIFIED (2026-10-03): the API version that introduced System.Comparator; version 1.0.0 of this skill stated 60.0, and the 262 references give no version.

@JsonAccess

@JsonAccess(serializable=... deserializable=...) sits at class level and accepts never, sameNamespace, samePackage, or always. If it restricts the operation, a runtime JSONException is thrown. In API 49.0 and later the default for both is sameNamespace. A subclass doesn't inherit the annotation.


Common Patterns

Pattern 1: Account plus computed count for a component

A top-level AccountRow class with @AuraEnabled properties, a separate with sharing controller that queries, aggregates, and builds rows, and a test class. Full deployable listing in references/code-examples.md.

Pattern 2: Several sort orders with Comparator<T>

One comparator class per order, each null-safe, selected at the call site. The wrapper class stays unchanged when a new order is added.

Pattern 3: REST request and response wrapper

A typed class deserialized with (MyRequest) JSON.deserialize(RestContext.request.requestBody.toString(), MyRequest.class). Add @JsonAccess only if a managed package or another namespace must serialize or deserialize it.


Decision Guidance

SituationRecommended ApproachReason
Component needs sObject plus computed fieldsTop-level wrapper class with @AuraEnabled propertiesInner classes as parameters or return values aren't supported
Component sends the wrapper back to ApexAdd {get; set;} to each @AuraEnabled propertyRequired for custom-class parameters
One fixed sort orderComparable on the wrapperOne contract, no extra class
Several sort ordersComparator<T> classesOrder chosen at the call site
REST payload in the same namespaceTyped class, no @JsonAccessDefault sameNamespace already allows it
Payload crosses a namespace or package@JsonAccess with the narrowest value that worksRestrictive access throws JSONException
Wrapper used only inside a serviceInner class, no annotationsSmallest surface

  1. Name the consumer and direction. Component return, component parameter, REST, or internal.
  2. Place the class. Top-level for components; inner is fine for internal use.
  3. Annotate the minimum. @AuraEnabled on properties the client reads; getters and setters where the client sends data; @JsonAccess only for cross-namespace serialization.
  4. Add sorting. Comparable or Comparator<T>, with an explicit null rule.
  5. Keep data access out of the wrapper. Query and aggregate in a with sharing controller or service; the wrapper constructor assigns fields.
  6. Test. 200-row build, null sort keys, null list entries, empty list; then run python3 scripts/check_apex_wrapper_class_patterns.py --manifest-dir force-app/main/default/classes.

Review Checklist


Salesforce-Specific Gotchas

See references/gotchas.md. The one that breaks the most components: returning an inner class from an @AuraEnabled method, which the LWC guide lists as unsupported.


Output Artifacts

ArtifactDescription
Wrapper classTop-level class with @AuraEnabled properties, or an inner class for internal use
Controllerwith sharing class with static @AuraEnabled methods
Comparator classesNull-safe sort orders
Test classBulk and null-case coverage

Package files