JSON.deserialize(jsonString, YourClass.class) if you know the exact structure of the incoming data. This gives you a structured Apex object so you can access data easily (e.g., user.email). Use JSON.deserializeUntyped(jsonString) if the data structure changes dynamically or you only need a few specific fields. This returns a generic Map or List, requiring you to cast values manually.
Almost every Salesforce integration involves a common step: an external system sends you a JSON string, and you need to extract its values using Apex. Salesforce provides two primary methods to accomplish this. In this guide, we will break down both approaches with working examples, highlight common developer pitfalls, and give you a simple rule of thumb for choosing the right one.
📊 Typed vs Untyped at a Glance
| Feature | JSON.deserialize (Typed) | JSON.deserializeUntyped |
|---|---|---|
| Requires an Apex Class? | Yes, structured to match the JSON | No |
| Return Type | An instance of your specific class | Map<String, Object> (objects) or List<Object> (arrays) |
| Accessing Values | Dot notation (e.g., user.address.city) | get('key') followed by a manual data cast |
| Handling Extra Keys | Ignored (unless you use deserializeStrict) | Retained, allowing you to iterate over them |
| Error Catching | Catches structure typos at compile-time | Errors typically surface at run-time |
| Best Use Case | Stable, well-documented API responses | Dynamic APIs, webhooks, or extracting just 1-2 fields |
📦 JSON.deserialize: Parse JSON into an Apex Class
To use typed deserialization, you write an Apex wrapper class where the variable names directly match the JSON keys. Nested JSON objects are represented by inner classes, and JSON arrays are mapped to Lists.
Imagine receiving this JSON response representing an employee:
{
"name": "John Doe",
"age": 30,
"department": "Sales",
"address": { "city": "Austin", "country": "US" },
"skills": ["Apex", "LWC"]
}
Here is how you would structure the corresponding Apex class:
public class EmployeeParser {
public class Address {
public String city;
public String country;
}
public class Employee {
public String name;
public Integer age;
public String department;
public Address address;
public List<String> skills;
}
// Method to parse a single JSON object
public static Employee parseOne(String jsonText) {
return (Employee) JSON.deserialize(jsonText, Employee.class);
}
// Method to parse a JSON array of objects
public static List<Employee> parseMany(String jsonText) {
return (List<Employee>) JSON.deserialize(jsonText, List<Employee>.class);
}
}
You can test this easily in the Developer Console > Debug > Open Execute Anonymous Window:
String body = '{"name":"John Doe","age":30,"department":"Sales","address":{"city":"Austin","country":"US"},"skills":["Apex","LWC"]}';
EmployeeParser.Employee emp = EmployeeParser.parseOne(body);
System.debug(emp.name + ' works in ' + emp.department + ', ' + emp.address.city);
System.debug('First skill: ' + emp.skills[0]);
John Doe works in Sales, AustinFirst skill: Apex
Pro Tip: Writing wrapper classes manually for massive API responses is tedious. You can use free web tools like JSON2Apex to automatically generate your wrapper classes from a sample JSON payload.
🧩 JSON.deserializeUntyped: Parse JSON Without a Class
When you use the untyped method, Salesforce returns a generic Object. You must cast this object to a Map<String, Object> (if the JSON starts with curly braces {}) or a List<Object> (if it starts with square brackets []).
Extracting Simple Values
String body = '{"name":"John","age":30,"isStudent":false}';
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(body);
String name = (String) data.get('name');
Integer age = (Integer) data.get('age');
Boolean isStudent = (Boolean) data.get('isStudent');
Navigating Nested Objects and Arrays
When data is nested, you have to cast it at every level:
String body = '{"team":"Support","members":[{"name":"Alice","age":22},{"name":"Bob","age":23}]}';
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(body);
// Cast the nested array into a List of Objects
List<Object> members = (List<Object>) data.get('members');
for (Object item : members) {
// Cast each item in the array to a Map
Map<String, Object> member = (Map<String, Object>) item;
System.debug(member.get('name') + ' is ' + member.get('age'));
}
Handling Unknown or Dynamic Keys
Untyped parsing shines when you don't know the keys in advance. For example, a currency exchange API might return dynamic keys based on the requested currencies:
String body = '{"base":"USD","rates":{"EUR":0.92,"INR":83.1,"GBP":0.79}}';
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(body);
Map<String, Object> rates = (Map<String, Object>) data.get('rates');
// Loop through dynamic keys dynamically
for (String code : rates.keySet()) {
Decimal rate = Decimal.valueOf(String.valueOf(rates.get(code)));
System.debug('1 USD = ' + rate + ' ' + code);
}
A Helper Utility for Safe Parsing
Real-world APIs sometimes omit fields entirely. Calling .get() on a null value will crash your code. Here is a handy utility class to keep your untyped parsing safe and clean:
public class JsonReader {
public static String getString(Map<String, Object> data, String key) {
Object value = (data == null) ? null : data.get(key);
return (value == null) ? null : String.valueOf(value);
}
public static Decimal getDecimal(Map<String, Object> data, String key) {
Object value = (data == null) ? null : data.get(key);
return (value == null) ? null : Decimal.valueOf(String.valueOf(value));
}
public static Map<String, Object> getMap(Map<String, Object> data, String key) {
Object value = (data == null) ? null : data.get(key);
return (value instanceof Map<String, Object>) ? (Map<String, Object>) value : null;
}
}
🚨 Handle Errors Before They Reach Users
Never trust external data blindly. Wrap your JSON parsing in a try-catch block to handle malformed data gracefully.
try {
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(body);
// process data...
} catch (JSONException e) {
// Triggers if the JSON syntax is broken (e.g., missing quotes or brackets)
System.debug(LoggingLevel.ERROR, 'Invalid JSON format: ' + e.getMessage());
} catch (TypeException e) {
// Triggers if you try to cast an array into a Map, or vice versa
System.debug(LoggingLevel.ERROR, 'Unexpected data shape: ' + e.getMessage());
}
- Calling
get()on the raw result: BecausedeserializeUntypedreturns anObject,result.get('key')won't compile. You must cast it to a Map first. - Casting numbers too strictly: Untyped parsing converts whole numbers to
Integerand decimals toDecimal. If an API returns10today but10.5tomorrow, casting to(Integer)will fail. Always safely read numbers asDecimal.valueOf(String.valueOf(value)). - Assuming a key always exists: Missing keys return
null. Attempting to chain methods on a null value throws aNullPointerException. - Using Apex Reserved Words: If a JSON key is named
date,number, orcurrency, it will fail typed parsing because these are reserved words in Apex. You must either use untyped parsing or string-replace the keys (e.g., replace"date":with"date_x":) before parsing.
⚖️ Which One Should You Use?
- Use
JSON.deserializeas your default for integrations. It self-documents the API structure and keeps your business logic clean. - Use
JSON.deserializeStrictif you need strict validation. It throws an error if the API sends unexpected fields, alerting you that a partner changed their payload structure. - Use
JSON.deserializeUntypedwhen you are dealing with webhooks with dynamic properties, or when you only need to extract a single ID from a massive payload without writing a 50-line wrapper class. - Use
System.JSONParser(Token parsing) only when dealing with extremely large JSON files (multi-megabyte) that risk hitting Apex heap size limits.
- The Core Rule: Known API format =
JSON.deserialize. Unknown or changing format =JSON.deserializeUntyped. - Typed Benefits: Easy dot-notation access, cleaner code, and catching type mismatches at compile time.
- Untyped Benefits: No wrapper classes required, ability to loop over dynamic keys.
- Untyped Drawbacks: Verbose casting, manual null checking, and errors hide until run-time.
❓ Core Q&A
Q: What is the main difference between JSON.deserialize and JSON.deserializeUntyped?
A: JSON.deserialize maps JSON directly into an Apex class you define, allowing you to use simple dot notation. JSON.deserializeUntyped requires no class but returns generic Objects, forcing you to manually cast every value you extract.
Q: Why do I get a compile error when I call get() on the untyped result?
A: Both parsing methods initially return the base Object type, which does not possess a get() method. You must explicitly cast the result to Map<String, Object> before attempting to read keys.
Q: How do I parse a JSON array that is at the root of the response?
A: For typed parsing, use (List<YourClass>) JSON.deserialize(jsonText, List<YourClass>.class). For untyped parsing, cast the root result to a List<Object> instead of a Map.
Q: Can I deserialize JSON straight into a standard Salesforce sObject like Account or Contact?
A: Yes! As long as the JSON keys exactly match the field API names (like Name, Industry, or CustomField__c), you can do (Account) JSON.deserialize(jsonText, Account.class).
Q: How do I handle extremely large JSON files that hit the Apex Heap Size limit?
A: If your payload is massive, neither deserialize nor deserializeUntyped will work efficiently because they load the entire structure into memory. Instead, use the JSONParser class to read the payload sequentially, token by token.
Follow up question to consider: Now that you know how to parse incoming data, do you know how to structure and send outgoing JSON securely using Named Credentials?
Comments
Questions, corrections and your own experience are all welcome.
Post a Comment