Part 41: Casting in Apex
Welcome back to the Salesforce series. In the previous installments we covered Apex data types, variables, operators, and control flow. Now it is time to talk about casting — the mechanism that lets you convert a value from one type to another. Casting shows up constantly in real Apex code, especially when you work with triggers, dynamic SOQL, JSON deserialization, and generic utility methods. Understanding it well will save you from mysterious TypeException errors at runtime.
This is Part 41 of the series, Topic 3, Section 5. It is a shorter section, but every concept here is essential.
What Is Casting?
Casting is the process of converting a value from one data type to another. In Apex, every variable has a declared type. When you need to assign a value of one type to a variable of a different type, a cast is required.
There are two broad reasons you need casting:
- Type compatibility — You have a value in one type but the method or variable you are working with expects a different type.
- Working with generic types — Salesforce returns data in generic forms (SObject, Object) and you need to convert it to a specific type to access its fields.
Compile-Time vs Runtime Type Checking
Apex is a strongly typed language. The compiler checks types at compile time and will refuse to compile code that has obvious type mismatches. However, some type conversions can only be verified at runtime. When a runtime cast fails, Apex throws a System.TypeException.
// Compile-time error — the compiler catches this immediately
// Integer x = 'hello'; // Won't compile
// Runtime error — compiles fine, but fails when executed
Object obj = 'hello';
Integer x = (Integer) obj; // TypeException at runtime
Implicit Casting (Widening)
Implicit casting happens automatically when Apex converts a value from a smaller or more specific type to a larger or more general type. No special syntax is required. The compiler handles it for you because no data is lost in the conversion.
Common Implicit Conversions
// Integer to Long
Integer i = 42;
Long l = i; // Automatic — no cast needed
// Integer to Double
Integer count = 10;
Double d = count; // 10 becomes 10.0
// Integer to Decimal
Integer qty = 5;
Decimal price = qty; // 5 becomes 5.0
// Long to Double
Long bigNum = 100000L;
Double bigDbl = bigNum; // Automatic
// Integer to Double in arithmetic
Integer a = 7;
Integer b = 2;
Double result = a; // result is 7.0
// Note: a / b is still integer division (3), not 3.5
// To get 3.5, cast first: Double result2 = (Double) a / b;
The key rule is that widening conversions are safe because the target type can represent every possible value of the source type. An Integer can always fit inside a Long, and a Long can always fit inside a Double (though very large Long values may lose precision in Double).
Explicit Casting (Narrowing)
Explicit casting is required when converting from a larger or more general type to a smaller or more specific type. You must use the (Type) syntax to tell the compiler you accept the risk of data loss or a runtime exception.
// Double to Integer — fractional part is truncated
Double price = 29.99;
Integer rounded = (Integer) price; // 29, not 30
// Decimal to Integer
Decimal total = 150.75;
Integer truncated = (Integer) total; // 150
// Object to String
Object obj = 'Hello World';
String greeting = (String) obj; // Works because obj actually holds a String
// Object to Integer
Object numObj = 42;
Integer num = (Integer) numObj; // Works because numObj actually holds an Integer
When Data Loss Occurs
Narrowing casts can silently lose data. The fractional part of a Double is truncated, not rounded.
Double d1 = 9.1;
Double d2 = 9.9;
Integer i1 = (Integer) d1; // 9
Integer i2 = (Integer) d2; // 9 — NOT 10
// If you want rounding, use the Math class
Integer i3 = (Integer) Math.round(d2); // 10
Casting Primitive Types
Not all type conversions use the cast operator. For many primitive conversions, Apex provides explicit conversion methods. Understanding when to use the cast operator versus a conversion method is important.
Conversion Methods vs Cast Operator
| Conversion | Method | Cast Operator Works? |
|---|---|---|
| String to Integer | Integer.valueOf('42') | No |
| String to Long | Long.valueOf('100000') | No |
| String to Double | Double.valueOf('3.14') | No |
| String to Decimal | Decimal.valueOf('99.99') | No |
| String to Boolean | Boolean.valueOf('true') | No |
| String to Date | Date.valueOf('2026-04-06') | No |
| String to Datetime | Datetime.valueOf('2026-04-06 10:30:00') | No |
| Integer to String | String.valueOf(42) | No |
| Double to Integer | — | Yes: (Integer) myDouble |
| Object to String | — | Yes: (String) myObj |
| Integer to Double | — | Yes (implicit) |
The cast operator (Type) works when the underlying runtime value is actually compatible with the target type. You cannot cast a String to an Integer with (Integer) myString — you must use Integer.valueOf().
// String to numeric types — use conversion methods
String priceStr = '49.99';
Decimal priceDecimal = Decimal.valueOf(priceStr);
Double priceDouble = Double.valueOf(priceStr);
// String to Date
String dateStr = '2026-04-06';
Date publishDate = Date.valueOf(dateStr);
// String to Datetime
String dtStr = '2026-04-06 14:30:00';
Datetime publishDatetime = Datetime.valueOf(dtStr);
// Integer to String
Integer count = 150;
String countStr = String.valueOf(count);
// Boolean conversion
String boolStr = 'true';
Boolean isActive = Boolean.valueOf(boolStr); // true
Handling Invalid Conversions
Conversion methods throw exceptions when the input is not valid.
try {
Integer bad = Integer.valueOf('not a number');
} catch (TypeException e) {
System.debug('Conversion failed: ' + e.getMessage());
}
Casting sObjects
This is where casting becomes essential in everyday Salesforce development. The SObject type is the generic base type for all standard and custom objects. Salesforce frequently returns data as SObject or List<SObject>, and you must cast to a specific type to access object-specific fields.
The Generic SObject Type
Every Salesforce object — Account, Contact, Opportunity, MyCustomObject__c — is a subtype of SObject. This means any specific object can be assigned to a variable of type SObject.
// All of these are valid
SObject genericRecord = new Account(Name = 'Acme Corp');
SObject genericContact = new Contact(LastName = 'Smith');
SObject genericOpp = new Opportunity(Name = 'Big Deal', StageName = 'Prospecting', CloseDate = Date.today());
However, once you store a record in an SObject variable, you lose access to the specific fields through dot notation.
SObject generic = new Account(Name = 'Acme Corp');
// generic.Name — this won't compile for most fields
// You must either cast or use generic get/put methods
// Option 1: Cast to specific type
Account acc = (Account) generic;
System.debug(acc.Name); // 'Acme Corp'
// Option 2: Use generic get method
String name = (String) generic.get('Name');
System.debug(name); // 'Acme Corp'
Upcasting
Upcasting is the conversion from a specific type to a more general type. In Apex sObject terms, it means going from a specific object type (Account, Contact) to the generic SObject type. Upcasting is always safe and happens implicitly.
// Upcasting — specific to generic
Account acc = new Account(Name = 'Acme Corp');
SObject generic = acc; // Implicit upcast — always safe
Contact con = new Contact(LastName = 'Smith');
SObject genericCon = con; // Also safe
// Upcasting to Object (the most generic type)
Object mostGeneric = acc; // Account -> Object, always safe
When Upcasting Is Useful
Upcasting is useful when you write methods that need to work with any object type.
public static void logRecord(SObject record) {
System.debug('Record type: ' + record.getSObjectType());
System.debug('Record Id: ' + record.get('Id'));
}
// Can be called with any object type
Account acc = [SELECT Id, Name FROM Account LIMIT 1];
Contact con = [SELECT Id, Name FROM Contact LIMIT 1];
logRecord(acc); // Account upcasts to SObject automatically
logRecord(con); // Contact upcasts to SObject automatically
Downcasting
Downcasting is the conversion from a generic type to a more specific type. In sObject terms, it means going from SObject to Account, Contact, or another specific type. Downcasting requires an explicit cast and can fail at runtime if the actual object is not the expected type.
// Downcasting — generic to specific
SObject generic = new Account(Name = 'Acme Corp');
Account acc = (Account) generic; // Explicit downcast — works because it's actually an Account
// This would fail at runtime
SObject genericContact = new Contact(LastName = 'Smith');
// Account bad = (Account) genericContact; // TypeException!
Why Downcasting Matters
You need downcasting any time Salesforce gives you data in a generic form:
- Trigger context variables —
Trigger.newisList<SObject> - Dynamic SOQL —
Database.query()returnsList<SObject> - Describe results —
SObject.newSObject()returnsSObject - Generic utility methods — methods that accept
SObjectparameters
The instanceof Operator
The instanceof operator checks whether an object is an instance of a specific type. It returns a Boolean and is your primary tool for safe downcasting.
SObject record = new Account(Name = 'Test');
if (record instanceof Account) {
Account acc = (Account) record;
System.debug('It is an Account: ' + acc.Name);
} else if (record instanceof Contact) {
Contact con = (Contact) record;
System.debug('It is a Contact: ' + con.LastName);
}
Safe Casting Pattern
Always check with instanceof before downcasting when the type is not guaranteed.
public static void processRecord(SObject record) {
if (record instanceof Account) {
Account acc = (Account) record;
// Access Account-specific fields safely
System.debug('Account Name: ' + acc.Name);
System.debug('Industry: ' + acc.Industry);
} else if (record instanceof Contact) {
Contact con = (Contact) record;
System.debug('Contact Name: ' + con.FirstName + ' ' + con.LastName);
System.debug('Email: ' + con.Email);
} else {
System.debug('Unknown type: ' + record.getSObjectType());
}
}
Using instanceof with Apex Classes
The instanceof operator also works with Apex class hierarchies and interfaces.
public interface Discountable {
Decimal getDiscount();
}
public virtual class Product {
public String name;
}
public class PremiumProduct extends Product implements Discountable {
public Decimal getDiscount() {
return 0.15;
}
}
Object item = new PremiumProduct();
System.debug(item instanceof Product); // true
System.debug(item instanceof PremiumProduct); // true
System.debug(item instanceof Discountable); // true
Casting Collections
Casting collections is one of the trickier areas in Apex. You can cast a List<SObject> to a List<Account>, but there are rules and limitations to be aware of.
List Casting
// Creating a list of specific type and assigning to generic list
List<Account> accounts = new List<Account>();
accounts.add(new Account(Name = 'Acme'));
accounts.add(new Account(Name = 'Globex'));
// Upcast — List<Account> to List<SObject>
List<SObject> genericList = accounts; // Implicit upcast works
// Downcast — List<SObject> to List<Account>
List<SObject> queryResults = [SELECT Id, Name FROM Account LIMIT 5];
List<Account> accountList = (List<Account>) queryResults; // Explicit downcast
Map Casting
Maps with Id keys follow similar rules.
// Upcast
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account LIMIT 5]);
Map<Id, SObject> genericMap = accountMap; // Implicit upcast
// Downcast — requires explicit cast
Map<Id, SObject> genericQueryMap = new Map<Id, SObject>([SELECT Id, Name FROM Account LIMIT 5]);
Map<Id, Account> specificMap = (Map<Id, Account>) genericQueryMap;
Limitations and Workarounds
You cannot cast between unrelated collection types. The runtime type of the list must match the target.
// This will fail at runtime — the list was created as List<SObject>, not List<Account>
List<SObject> mixedList = new List<SObject>();
mixedList.add(new Account(Name = 'Acme'));
mixedList.add(new Account(Name = 'Globex'));
// This cast FAILS because the list's runtime type is List<SObject>
// List<Account> accList = (List<Account>) mixedList; // TypeException
// Workaround — iterate and cast individually
List<Account> accList = new List<Account>();
for (SObject record : mixedList) {
accList.add((Account) record);
}
This is a common gotcha. The cast of a collection succeeds only when the list was originally created as the specific type. A List<SObject> that happens to contain only Account records is still a List<SObject> at runtime, not a List<Account>.
Casting in Trigger Context
This is arguably the most common place you will encounter casting in real Salesforce development. Trigger context variables — Trigger.new, Trigger.old, Trigger.newMap, and Trigger.oldMap — all use generic sObject types.
Trigger.new and Trigger.old
Trigger.new returns List<SObject> and Trigger.old returns List<SObject>. To access fields specific to the object your trigger is on, you must cast.
trigger AccountTrigger on Account (before insert, before update) {
// Trigger.new is List<SObject>, but since this trigger is on Account,
// every element is guaranteed to be an Account
// Pattern 1: Cast the entire list
List<Account> newAccounts = (List<Account>) Trigger.new;
for (Account acc : newAccounts) {
if (String.isBlank(acc.Industry)) {
acc.Industry = 'Other';
}
}
// Pattern 2: Cast inside the loop (also valid)
for (SObject record : Trigger.new) {
Account acc = (Account) record;
if (String.isBlank(acc.Description)) {
acc.Description = 'Created on ' + Date.today();
}
}
// Pattern 3: Iterate directly with the specific type
// This works because Apex can implicitly downcast in for-each loops
// when the trigger is on a specific object
for (Account acc : (List<Account>) Trigger.new) {
System.debug(acc.Name);
}
}
Trigger.newMap and Trigger.oldMap
Trigger.newMap returns Map<Id, SObject> and Trigger.oldMap returns Map<Id, SObject>.
trigger OpportunityTrigger on Opportunity (before update) {
// Cast the maps
Map<Id, Opportunity> newMap = (Map<Id, Opportunity>) Trigger.newMap;
Map<Id, Opportunity> oldMap = (Map<Id, Opportunity>) Trigger.oldMap;
for (Id oppId : newMap.keySet()) {
Opportunity newOpp = newMap.get(oppId);
Opportunity oldOpp = oldMap.get(oppId);
// Detect stage change
if (newOpp.StageName != oldOpp.StageName) {
System.debug('Stage changed from ' + oldOpp.StageName + ' to ' + newOpp.StageName);
}
}
}
Trigger Handler Pattern with Casting
In a well-structured trigger framework, you typically cast once in the handler and pass the typed collections to your methods.
public class AccountTriggerHandler {
public static void beforeInsert(List<SObject> newRecords) {
List<Account> newAccounts = (List<Account>) newRecords;
setDefaultIndustry(newAccounts);
validateBillingAddress(newAccounts);
}
public static void beforeUpdate(List<SObject> newRecords, Map<Id, SObject> oldRecordsMap) {
List<Account> newAccounts = (List<Account>) newRecords;
Map<Id, Account> oldAccountMap = (Map<Id, Account>) oldRecordsMap;
detectNameChanges(newAccounts, oldAccountMap);
}
private static void setDefaultIndustry(List<Account> accounts) {
for (Account acc : accounts) {
if (String.isBlank(acc.Industry)) {
acc.Industry = 'Other';
}
}
}
private static void validateBillingAddress(List<Account> accounts) {
for (Account acc : accounts) {
if (String.isBlank(acc.BillingCity) && acc.AnnualRevenue > 1000000) {
acc.addError('High-value accounts must have a billing city.');
}
}
}
private static void detectNameChanges(List<Account> newAccounts, Map<Id, Account> oldMap) {
for (Account acc : newAccounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Name != oldAcc.Name) {
System.debug('Account renamed: ' + oldAcc.Name + ' -> ' + acc.Name);
}
}
}
}
Common Casting Errors
TypeException: Invalid Conversion
This is the most common casting error. It happens when you try to cast a value to a type that does not match its runtime type.
// Example 1: Casting to the wrong sObject type
SObject record = new Contact(LastName = 'Smith');
Account acc = (Account) record;
// System.TypeException: Invalid conversion from runtime type Contact to Account
// Example 2: Casting Object to the wrong primitive type
Object obj = 'Hello';
Integer num = (Integer) obj;
// System.TypeException: Invalid conversion from runtime type String to Integer
// Example 3: Casting a List<SObject> that was not created as the target type
List<SObject> records = new List<SObject>();
records.add(new Account(Name = 'Test'));
List<Account> accounts = (List<Account>) records;
// System.TypeException: Invalid conversion from runtime type List<SObject> to List<Account>
Debugging Tips
- Use
System.debug()to inspect the runtime type before casting.
SObject record = getRecordFromSomewhere();
System.debug('Runtime type: ' + record.getSObjectType()); // Account, Contact, etc.
System.debug('Is Account? ' + (record instanceof Account));
- Use
getSObjectType()for sObjects.
SObject record = Trigger.new[0];
Schema.SObjectType objType = record.getSObjectType();
System.debug(objType); // Account
System.debug(objType == Account.SObjectType); // true
- Wrap risky casts in try-catch blocks when dealing with data from external sources.
public static Account safelyConvertToAccount(SObject record) {
try {
return (Account) record;
} catch (TypeException e) {
System.debug('Cannot convert to Account: ' + e.getMessage());
return null;
}
}
Casting Scenarios — Quick Reference
| Scenario | Syntax | Safe? | Notes |
|---|---|---|---|
| Integer to Long | Long l = myInt; | Yes | Implicit widening |
| Integer to Double | Double d = myInt; | Yes | Implicit widening |
| Double to Integer | Integer i = (Integer) myDbl; | Lossy | Truncates fractional part |
| String to Integer | Integer.valueOf(str) | Throws on bad input | Use conversion method |
| String to Date | Date.valueOf(str) | Throws on bad input | Format: YYYY-MM-DD |
| Account to SObject | SObject s = myAccount; | Yes | Implicit upcast |
| SObject to Account | Account a = (Account) s; | Can fail | Check instanceof first |
| List<Account> to List<SObject> | List<SObject> l = myAccList; | Yes | Implicit upcast |
| List<SObject> to List<Account> | (List<Account>) myList | Can fail | Only if runtime type matches |
| Object to String | (String) myObj | Can fail | Runtime type must be String |
| Trigger.new to typed list | (List<Account>) Trigger.new | Yes | Guaranteed in Account trigger |
| JSON Object to typed class | Requires JSON.deserialize() | Can fail | Use target type parameter |
Practical Patterns
Generic Utility Methods
Casting enables you to write methods that work with any sObject type.
public class RecordUtils {
// Generic method to set a field on any sObject
public static void setField(List<SObject> records, String fieldName, Object value) {
for (SObject record : records) {
record.put(fieldName, value);
}
}
// Generic method to extract a field value from any sObject
public static List<Object> getFieldValues(List<SObject> records, String fieldName) {
List<Object> values = new List<Object>();
for (SObject record : records) {
values.add(record.get(fieldName));
}
return values;
}
// Check if a field changed between old and new versions
public static Boolean fieldChanged(SObject newRecord, SObject oldRecord, String fieldName) {
return newRecord.get(fieldName) != oldRecord.get(fieldName);
}
}
These methods accept SObject (the generic type) and work with any object. The caller upcasts automatically when passing Account, Contact, or any other object.
Dynamic SOQL Results
Database.query() returns List<SObject>. You must cast the results to the appropriate type.
String objectName = 'Account';
String query = 'SELECT Id, Name FROM ' + objectName + ' LIMIT 10';
List<SObject> results = Database.query(query);
// Cast to the expected type
if (!results.isEmpty() && results[0] instanceof Account) {
List<Account> accounts = (List<Account>) results;
for (Account acc : accounts) {
System.debug(acc.Name);
}
}
JSON Deserialization with Casting
When you deserialize JSON, the result often needs casting.
// Deserializing to a specific type
String jsonStr = '{"Name": "Acme Corp", "Industry": "Technology"}';
Account acc = (Account) JSON.deserialize(jsonStr, Account.class);
System.debug(acc.Name); // 'Acme Corp'
// Deserializing to a generic Object
String genericJson = '{"key": "value", "count": 42}';
Map<String, Object> parsed = (Map<String, Object>) JSON.deserializeUntyped(genericJson);
String key = (String) parsed.get('key');
Integer count = (Integer) parsed.get('count');
System.debug(key); // 'value'
System.debug(count); // 42
// Nested structures require chained casting
String nestedJson = '{"account": {"Name": "Acme"}, "tags": ["vip", "enterprise"]}';
Map<String, Object> nested = (Map<String, Object>) JSON.deserializeUntyped(nestedJson);
Map<String, Object> accMap = (Map<String, Object>) nested.get('account');
List<Object> tags = (List<Object>) nested.get('tags');
String firstTag = (String) tags[0];
System.debug(accMap.get('Name')); // 'Acme'
System.debug(firstTag); // 'vip'
Section Notes — Key Takeaways
- Implicit casting (widening) happens automatically and is always safe. Integer to Long, Integer to Double, specific sObject to SObject.
- Explicit casting (narrowing) requires the
(Type)syntax and can fail at runtime or lose data. Double to Integer truncates. SObject to Account can throw TypeException. - Conversion methods like
Integer.valueOf()andDate.valueOf()are required for String-to-other conversions. The cast operator does not work for these. - Trigger context variables are always generic (
List<SObject>,Map<Id, SObject>). Cast them to the specific object type at the top of your handler. - The
instanceofoperator is your safety net. Use it before downcasting when the type is not guaranteed. - Collection casting only works when the runtime type of the collection matches the target. A
List<SObject>created withnew List<SObject>()cannot be cast toList<Account>, even if it only contains Account records.
When Casting Is a Code Smell
Excessive casting can indicate a design problem. If you find yourself casting repeatedly, consider:
- Whether your method signatures should use more specific types
- Whether a generic utility is really needed, or if separate typed methods would be clearer
- Whether an interface or abstract class would provide better type safety than casting from Object
Casting is a tool, not a goal. Use it when Salesforce requires it (triggers, dynamic operations), and avoid it when strong typing can do the job.
Best Practices
-
Always check
instanceofbefore downcasting when the type is not guaranteed by context. In a trigger on Account, you knowTrigger.newcontains Account records, soinstanceofis unnecessary. In a generic utility method, it is essential. -
Cast once, use many times. In trigger handlers, cast
Trigger.newandTrigger.oldMapto typed variables at the top of the method, then pass those typed variables to helper methods. -
Prefer generic methods for reusability. Methods that accept
SObjectand useget()/put()can work with any object type without modification. -
Avoid unnecessary casting. If you already have a typed variable, do not upcast to SObject and then downcast back. That adds complexity with no benefit.
-
Use conversion methods for String conversions. Never try to cast a String to an Integer with
(Integer). UseInteger.valueOf()instead. -
Handle conversion failures gracefully. Wrap
valueOf()calls and risky casts in try-catch blocks, especially when processing external data. -
Understand collection casting limitations. When you need to convert a
List<SObject>to aList<Account>, check whether the list was originally created as aList<Account>. If not, iterate and cast individually. -
Use
getSObjectType()for runtime type inspection of sObject records. It returns aSchema.SObjectTypetoken that you can compare directly:record.getSObjectType() == Account.SObjectType.
What Comes Next
In Part 42, we will cover Debugging Apex — debug logs, System.debug statements, the Developer Console, log levels, checkpoints, anonymous execute, and strategies for efficiently tracking down bugs in your Apex code. Debugging is the companion skill to writing code, and mastering it will make everything else easier.
See you in the next one.