Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The quick version: the JSON does not match the shape you told Apex to expect. The message says which field and, usefully, the character position. Most of these come from one of three things: a number arriving as a string, a field name that does not match your property, or a reserved word you cannot use as a property name.

Two real messages from a scratch org:

JSON.deserialize('{"Name":"x","NumberOfEmployees":"notanumber"}', Account.class);
// System.JSONException: Cannot deserialize instance of int from VALUE_STRING
// value notanumber or request may be missing a required field at [line:1, column:12]

JSON.deserializeStrict('{"a":1,"unknown":2}', Simple.class);
// System.JSONException: Unknown field: Simple.unknown

strict or not, and why it matters

JSON.deserialize ignores fields it does not recognise. JSON.deserializeStrict throws on them. Neither is correct in general:

  • Use strict for a payload you control, or when a new field appearing should be treated as a change that needs handling. It catches typos in your own class immediately.
  • Use lenient for a third-party API you do not control, because the vendor will add fields without telling you and you do not want a release note to break your integration.

Picking lenient for convenience and then wondering why a value is always null is the common failure. The field name in your class must match the JSON exactly, including case, and a mismatch is silent.

The reserved word problem

Apex will not let you declare a property called currency, date, group, limit or a dozen others, and plenty of APIs use exactly those names. The fix is not a workaround, it is a supported pattern: deserialise into a map, rename, and re-serialise.

String fixed = body.replace('"currency":', '"currencyCode":');
Payment p = (Payment) JSON.deserialize(fixed, Payment.class);

A string replace is crude and works when the name is unambiguous. When it is not, go through Map<String, Object> and build the object by hand, which is more code and cannot be fooled by the same substring appearing in a value.

Numbers, which cause most of the rest

JSON has one number type. Apex has four. Integer, Long, Decimal and Double all round-trip differently, and a currency value declared as Double will eventually show a rounding error in a total someone cares about. Declare money as Decimal, always. A blind cast out of an untyped map is where this turns into Invalid conversion from runtime type.

A number sent as a string, "amount": "45.00", is not a number to the parser, and it is extremely common from systems whose own types are loose. Declare the property as String and convert yourself, or fix it at the sender. Do not declare it as Decimal and hope.

Nulls and missing fields are not the same

A field absent from the JSON leaves your property at its default, which for an object is null and for a primitive wrapper is also null. A field present with an explicit null does the same thing. So after deserialisation you cannot tell “they did not send it” from “they sent it empty”, and for a PATCH-style integration that distinction is the whole semantics. If you need it, parse to Map<String, Object> and check containsKey.

Dates

Apex expects ISO 8601. A date in any other format fails to deserialise into a Date or Datetime, and the message points at the position rather than explaining the format. Declare the property as String and parse explicitly when the source is not reliably ISO, because a failed parse you control gives a better error than a deserialisation that stops the whole payload.

Frequently asked

Can I deserialise directly into an sObject? Yes, and field names must be API names. It works well for simple cases and badly for anything with relationships.

How do I handle an array at the top level? Cast to List<YourType>: (List<Order>) JSON.deserialize(body, List<Order>.class).

Is there a size limit? The deserialised object counts against heap, which is 6 MB synchronously and 12 MB asynchronously. A large payload is a heap problem before it is a parsing problem.