How do I get started working with JSON in Apex?

Apex provides multiple routes to achieving JSON serialization and deserialization of data structures. This answer summarizes use cases and capabilities of untyped deserialization, typed (de)serialization, manual implementations using JSONGenerator and JSONParser, and tools available to help support these uses. It is not intended to answer every question about JSON, but to provide an introduction, overview, and links to other resources.

Summary

Apex can serialize and deserialize JSON to strongly-typed Apex classes and also to generic collections like Map<String, Object> and List<Object>. In most cases, it's preferable to define Apex classes that represent data structures and utilize typed serialization and deserialization with JSON.serialize()/JSON.deserialize(). However, some use cases require applying untyped deserialization with JSON.deserializeUntyped().

The JSONGenerator and JSONParser classes are available for manual implementations and should be used only where automatic (de)serialization is not practicable, such as when keys in JSON are reserved words or invalid identifiers in Apex, or when low-level access is required.

The key documentation references are the JSON class in the Apex Developer Guide and the section JSON Support. Other relevant documentation is linked from those pages.

Complex Types in Apex and JSON

JSON offers maps (or objects) and lists as its complex types. JSON lists map to Apex List objects. JSON objects can map to either Apex classes, with keys mapping to instance variables, or Apex Map objects. Apex classes and collections can be intermixed freely to construct the right data structures for any particular JSON objective.

Throughout this answer, we'll use the following JSON as an example:

{
    "errors": [ "Data failed validation rules" ],
    "message": "Please edit and retry",
    "details": {
        "record": "001000000000001",
        "record_type": "Account"
    }
}

This JSON includes two levels of nested objects, as well as a list of primitive values.

Typed Serialization with JSON.serialize() and JSON.deserialize()

The methods JSON.serialize() and JSON.deserialize() convert between JSON and typed Apex values. When using JSON.deserialize(), you must specify the type of value you expect the JSON to yield, and Apex will attempt to deserialize to that type. JSON.serialize() accepts both Apex collections and objects, in any combination that's convertible to legal JSON.

These methods are particularly useful when converting JSON to and from Apex classes, which is in most circumstances the preferred implementation pattern. The JSON example above can be represented with the following Apex class:


public class Example {
    public List<String> errors;
    public String message;
    
    public class ExampleDetail {
        Id record;
        String record_type;
    }
    
    public ExampleDetail details;
}

To parse JSON into an Example instance, execute

Example ex = (Example)JSON.deserialize(jsonString, Example.class);

Alternately, to convert an Example instance into JSON, execute

String jsonString = JSON.serialize(ex);

Note that nested JSON objects are modeled with one Apex class per level of structure. It's not required for these classes to be inner classes, but it is a common implementation pattern. Apex only allows one level of nesting for inner classes, so deeply-nested JSON structures often convert to Apex classes with all levels of structure defined in inner classes at the top level.

JSON.serialize() and JSON.deserialize() can be used with Apex collections and classes in combination to represent complex JSON data structures. For example, JSON that stored Example instances as the values for higher-level keys:

{
    "first": { /* Example instance */ },
    "second": { /* Example instance */},
    /* ... and so on... */
}

can be serialized from, and deserialized to, a Map<String, Example> value in Apex.

It should be noted that this approach will not work where the JSON to be deserialized cannot be directly mapped to Apex class attributes (e.g. because the JSON property names are Apex reserved words or are invalid as Apex identifiers (e.g. contain hyphens or other invalid characters).

For more depth on typed serialization and deserialization, review the JSON class documentation. Options are available for:

  • Suppression of null values
  • Pretty-printing generated JSON
  • Strict deserialization, which fails on unexpected attributes

Untyped Deserialization with JSON.deserializeUntyped()

In some situations, it's most beneficial to deserialize JSON into Apex collections of primitive values, rather than into strongly-typed Apex classes. For example, this can be a valuable approach when the structure of the JSON may change in ways that aren't compatible with typed deserialization, or which would require features that Apex does not offer like algebraic or union types.

Using the JSON.deserializeUntyped() method yields an Object value, because Apex doesn't know at compile time what type of value the JSON will produce. It's necessary when using this method to typecast values pervasively.

Take, for example, this JSON, which comes in multiple variants tagged by a "scope" value:

{
    "scope": "Accounts",
    "data": {
        "payable": 100000,
        "receivable": 40000
    }
}

or

{
    "scope": {
        "division": "Sales",
        "organization": "International"
    },
    "data": {
        "closed": 400000
    }
}

JSON input that varies in this way cannot be handled with strongly-typed Apex classes because its structure is not uniform. The values for the keys scope and data have different types.

This kind of JSON structure can be deserialized using JSON.deserializeUntyped(). That method returns an Object, an untyped value whose actual type at runtime will reflect the structure of the JSON. In this case, that type would be Map<String, Object>, because the top level of our JSON is an object. We could deserialize this JSON via

Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonString);

The untyped nature of the value we get in return cascades throughout the structure, because Apex doesn't know the type at compile time of any of the values (which may, as seen above, be heterogenous) in this JSON object.

As a result, to access nested values, we must write defensive code that inspects values and typecasts at each level. The example above will throw a TypeException if the resulting type is not what is expected.

To access the data for the first element in the above JSON, we might do something like this:

Object result = JSON.deserializeUntyped(jsonString);

if (result instanceof Map<String, Object>) {
    Map<String, Object> resultMap = (Map<String, Object>)result;
    if (resultMap.get('scope') == 'Accounts' &&
        resultMap.get('data') instanceof Map<String, Object>) {
        Map<String, Object> data = (Map<String, Object>)resultMap.get('data');
    
        if (data.get('payable') instanceof Integer) {
            Integer payable = (Integer)data.get('payable');
            
            AccountsService.handlePayables(payable);
        } else {
            // handle error
        }
    } else {
        // handle error
    }
} else {
    // handle error
}

While there are other ways of structuring such code, including catching JSONException and TypeException, the need to be defensive is a constant. Code that fails to be defensive while working with untyped values is vulnerable to JSON changes that produce exceptions and failure modes that won't manifest in many testing practices. Common exceptions include NullPointerException, when carelessly accessing nested values, and TypeException, when casting a value to the wrong type.

Manual Implementation with JSONGenerator and JSONParser

The JSONGenerator and JSONParser classes allow your application to manually construct and parse JSON.

Using these classes entails writing explicit code to handle each element of the JSON. Using JSONGenerator and JSONParser typically yields much more complex (and much longer) code than using the built-in serialization and deserialization tools. However, it may be required in some specific applications. For example, JSON that includes Apex reserved words as keys may be handled using these classes, but cannot be deserialized to native classes because reserved words (like type and class) cannot be used as identifiers.

As a general guide, use JSONGenerator and JSONParser only when you have a specific reason for doing so. Otherwise, strive to use native serialization and deserialization, or use external tooling to generate parsing code for you (see below).

Generating Code with JSON2Apex

JSON2Apex is an open source Heroku application. JSON2Apex allows you to paste in JSON and generates corresponding Apex code to parse that JSON. The tool defaults to creating native classes for serialization and deserialization. It automatically detects many situations where explicit parsing is required and generates JSONParser code to deserialize JSON to native Apex objects.

JSON2Apex does not solve every problem related to using JSON, and generated code may require revision and tuning. However, it's a good place to start an implementation, particularly for users who are just getting started with JSON in Apex.

Common Workarounds

JSON attribute is a reserved word or invalid identifier

For example, you might have incoming JSON that looks like:

{"currency": "USD", "unitPrice" : 10.00, "_mode": "production"}

that you want to deserialize into a custom Apex Type:

public class MyStuff {
  String currency;
  Decimal unitPrice;
  String _mode;
}

But currency can't be used as a variable name because it is a reserved word, nor can _mode because it is not a legal Apex identifier.

One easy workaround is to rename the variable and preprocess the JSON before deserializing:

public class MyStuff {
  String currencyX;     // in JSON as currency
  Decimal unitPrice;
}

MyStuff myStuff = (MyStuff) JSON.deserialize(theJson.replace('"currency":','"currencyX":'),
                                             MyStuff.class);

However, note that this strategy can fail on large payloads. JSON2Apex is capable of generating manual deserialization code that handles invalid identifiers as well, and untyped deserialization is another option.


Woops, only just noticed I was supposed to edit the answer... sorry.

Great, detailed post from David on this!

Here is a short (supplementary) post:

  • JSON is very simple so start by understanding that: read this Introducing JSON page first, at least a couple of times
  • In 100% of my code I use the JSON class; I have used JSONGenerator and JSONParser 0% of the time. (See the last point below.)
  • If you do want generated classes then do explore what JSON2Apex produces.
  • To handle JSON where the keys are not legal Apex identifiers, using Apex Map<String, Object> works well. You can generate JSON by creating those Apex maps and then calling JSON.serialize and you can parse into those by calling JSON.deserializeUntyped.

Apex's nice initialisation syntax helps here too e.g.:

Map<String, Object> root = new Map<String, Object>{
    'awkward key' => 'awkward with "quotes" value',
    'nested object key' => new Map<String, Object>{
        'key1' => 'value1',
        'key2' => true,
        'key3' => 123.456,
        'key4' => null
    },
    'nested array key' => new List<Map<String, Object>>{
        new Map<String, Object>{
            'another key1' => 'value1',
            'another key2' => true
        },
        new Map<String, Object>{
            'another key1' => 'value2',
            'another key2' => false
        }
    }
};

String jsonString = JSON.serializePretty(root);
System.debug(jsonString);

produces:

{
  "nested array key" : [ {
    "another key2" : true,
    "another key1" : "value1"
  }, {
    "another key2" : false,
    "another key1" : "value2"
  } ],
  "nested object key" : {
    "key4" : null,
    "key3" : 123.456,
    "key2" : true,
    "key1" : "value1"
  },
  "awkward key" : "awkward with \"quotes\" value"
}

While the resulting key ordering is annoying, it is an implementation artefact; key ordering is not significant in JSON.