Skip to main content

Demystifying JSON.deserializeUntyped in Apex Salesforce with Code Examples

Updated Add a comment
In plain words: When working with API responses in Salesforce, use 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

FeatureJSON.deserialize (Typed)JSON.deserializeUntyped
Requires an Apex Class?Yes, structured to match the JSONNo
Return TypeAn instance of your specific classMap<String, Object> (objects) or List<Object> (arrays)
Accessing ValuesDot notation (e.g., user.address.city)get('key') followed by a manual data cast
Handling Extra KeysIgnored (unless you use deserializeStrict)Retained, allowing you to iterate over them
Error CatchingCatches structure typos at compile-timeErrors typically surface at run-time
Best Use CaseStable, well-documented API responsesDynamic 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]);
Debug Log Output:
John Doe works in Sales, Austin
First 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());
}
Common Mistakes That Cause Run-Time Errors:
  • Calling get() on the raw result: Because deserializeUntyped returns an Object, result.get('key') won't compile. You must cast it to a Map first.
  • Casting numbers too strictly: Untyped parsing converts whole numbers to Integer and decimals to Decimal. If an API returns 10 today but 10.5 tomorrow, casting to (Integer) will fail. Always safely read numbers as Decimal.valueOf(String.valueOf(value)).
  • Assuming a key always exists: Missing keys return null. Attempting to chain methods on a null value throws a NullPointerException.
  • Using Apex Reserved Words: If a JSON key is named date, number, or currency, 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.deserialize as your default for integrations. It self-documents the API structure and keeps your business logic clean.
  • Use JSON.deserializeStrict if 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.deserializeUntyped when 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.
Key Points: Typed vs Untyped JSON in Apex
  • 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.

Takeaway: If you know the shape of the incoming JSON, create a wrapper class and use JSON.deserialize. If the shape is unpredictable, use JSON.deserializeUntyped, cast your variables carefully, and code defensively against nulls.

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?

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.