System.JSONException: <message>
Apex couldn't parse a JSON string — unexpected character, missing comma, type mismatch in deserialize. The exception message says where in the JSON it choked. The fix depends on whether the producer or consumer is at fault.
Also seen asSystem.JSONException·Unexpected character·JSON parse error apex·Unable to deserialize
Apex's JSON.deserialize and JSON.deserializeUntyped are strict — they expect well-formed JSON matching the type signature you gave. Every tolerance is opt-in.
Common variants
"Illegal value for primitive"
public class Person { public Integer age; }
Person p = (Person) JSON.deserialize('{"age": "thirty"}', Person.class);
// JSONException: Illegal value for primitive: thirty
The JSON has "thirty" (string) but the Apex field expects Integer. Fix: convert the value at the source, or use a softer parse target.
"Unexpected character"
JSON.deserializeUntyped('{name: "Smith"}');
// JSONException: Unexpected character ('n')
Strict JSON requires keys to be quoted: {"name": "Smith"}. JavaScript-style unquoted keys aren't valid JSON.
"End of input expected"
A trailing comma, an extra }, or a stray character at the end. Validate with jq or jsonlint before passing to Apex.
"Apex type unsupported in JSON"
You tried to deserialize into an Apex type that contains a Map<Object, Object> or some other non-standard shape. JSON deserialize supports primitives, lists, maps with string keys, and SObjects — not arbitrary Apex types. Restructure your DTO.
Defensive parsing
For untrusted JSON, use deserializeUntyped and walk the result manually:
Object raw = JSON.deserializeUntyped(payload);
if (raw instanceof Map<String, Object>) {
Map<String, Object> obj = (Map<String, Object>) raw;
String name = (String) obj.get('name');
Integer age = obj.get('age') instanceof Integer ? (Integer) obj.get('age') : null;
// ...
}
This survives extra fields, missing fields, and unexpected types without throwing. The cost is verbosity.
When the JSON is huge
JSON.deserialize loads the entire string into memory + builds a fully-parsed Apex object graph. For large payloads (>1 MB) you may hit heap limits. Two options:
- Stream parse with
JSONParser:JSONParser parser = JSON.createParser(payload); while (parser.nextToken() != null) { if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'records') { // process records as they stream } } - Chunk the upstream payload so each piece fits comfortably under heap caps.
A subtle gotcha: reserved words
Apex has reserved words (new, class, for, etc.) that can't be Apex field names. If your incoming JSON has a key like "class": "premium", you can't deserialize into an Apex class with a field named class. Use JSON.deserializeUntyped and read the value out by key, or rename the JSON key upstream.
