A wrapper class is a plain Apex class — usually an inner class — whose only job is to bundle an sObject with extra fields the UI needs. The canonical example is a table where each row has a checkbox: the database has no checkbox field, so you wrap the sObject in a small class that adds a Boolean isSelected property. Wrapper classes are how Apex talks to Visualforce pages, LWCs, and Aura components when the standard sObject shape isn’t enough.
The canonical wrapper
public with sharing class AccountSelectController {
// Inner wrapper class — pairs an Account with UI-only state
public class AccountWrapper {
@AuraEnabled public Account record;
@AuraEnabled public Boolean isSelected;
@AuraEnabled public Decimal openOpportunitySum;
public AccountWrapper(Account a, Decimal sum) {
this.record = a;
this.isSelected = false;
this.openOpportunitySum = sum;
}
}
@AuraEnabled(cacheable=true)
public static List<AccountWrapper> getAccountsWithSums() {
List<AccountWrapper> rows = new List<AccountWrapper>();
Map<Id, Decimal> sumsByAccount = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId, SUM(Amount) sumAmt
FROM Opportunity
WHERE StageName != 'Closed Won' AND StageName != 'Closed Lost'
GROUP BY AccountId
]) {
sumsByAccount.put((Id) ar.get('AccountId'), (Decimal) ar.get('sumAmt'));
}
for (Account a : [SELECT Id, Name, Industry FROM Account LIMIT 50]) {
Decimal sum = sumsByAccount.containsKey(a.Id) ? sumsByAccount.get(a.Id) : 0;
rows.add(new AccountWrapper(a, sum));
}
return rows;
}
}
The LWC consuming this gets a JSON array where each element has record, isSelected, and openOpportunitySum — neatly structured for the template.
Why you need a wrapper
Plain sObjects can carry data, but they can’t carry UI-only flags or values that don’t exist as fields on the object. Common reasons to wrap:
- Selection state — a checkbox per row in a data table.
- Computed values — a rollup or count that isn’t worth storing on the record itself.
- Display strings — pre-formatted dates, currencies, or labels.
- Cross-object joins — bundling the Account with its primary Contact and most-recent Case in one structure.
- Validation results — pairing a record with a list of error messages per row.
- Page-specific shape — flattening a parent-child relationship into a row a table can render.
What makes it “wrapper-ish”
A wrapper class isn’t a special Apex construct — there’s no @Wrapper annotation, no compiler awareness. It’s just a regular class. The convention is:
- It usually lives as an inner class inside the controller that returns it, so it’s namespaced by use case.
- Its fields are marked
@AuraEnabledso LWC and Aura can read/write them. - It’s lightweight — just data, maybe a constructor, rarely methods.
The @AuraEnabled annotation is what makes the class serializable to JSON for the Lightning runtime. Without it, the LWC will see undefined for those fields.
Wrappers with @AuraEnabled(cacheable=true)
When your wire-service method is cacheable=true, the LWC @wire will cache the response and re-render on data change. Wrapper classes work seamlessly with this — they serialize cleanly, and the framework re-runs the wire when its arguments change.
Wrappers for inbound LWC data
The same pattern works in reverse. An LWC sending data to Apex can pass a JSON shape that matches the wrapper:
// LWC
import saveRows from '@salesforce/apex/AccountSelectController.saveRows';
saveRows({ rows: [
{ record: { Id: '001...', Industry: 'Technology' }, isSelected: true, openOpportunitySum: 50000 }
] });
@AuraEnabled
public static void saveRows(List<AccountWrapper> rows) {
List<Account> toUpdate = new List<Account>();
for (AccountWrapper w : rows) {
if (w.isSelected) {
toUpdate.add(w.record);
}
}
update toUpdate;
}
The framework deserializes the inbound JSON into the wrapper class using the field names, then your Apex code works with it like any other object.
Wrapper vs DTO vs anemic class
The Salesforce community has settled on “wrapper” as the catch-all term for these structures, but in classic OOP they’re called DTOs (Data Transfer Objects) or anemic classes (no behaviour, just data). The naming is the same; the purpose is the same. The reason “wrapper” stuck in Salesforce is that the most common case is wrapping an sObject with a flag.
Common pitfalls
- Forgetting
@AuraEnabled— the LWC will seeundefinedfor those fields. Required on every field exposed to Aura/LWC. - Putting business logic in the wrapper — keep wrappers data-only. Logic belongs in the controller or a service class.
- Returning enormous wrapper lists — wrappers carry overhead; for 50,000-row exports, return raw sObjects or a flat structure.
What interviewers are really looking for
The wrapper question is a LWC/UI integration check. Interviewers want to know you’ve actually built a Lightning component that needs row-level UI state, not just memorised theory. Mention @AuraEnabled on fields, inner classes inside controllers, and the round-trip serialization back from LWC to Apex — three concrete signals you’ve shipped this pattern.
Verified against: Apex Developer Guide — Classes, LWC Developer Guide — Apex Methods. Last reviewed 2026-05-17 for Spring ‘26 release.