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.

Briefly: you cast an Object to a type it is not. Apex checks casts at runtime, not compile time, so the code compiles fine and fails the first time the value is the wrong shape. The message names both types, which is the whole diagnosis.

Object o = 'hello';
Integer i = (Integer) o;
// System.TypeException: Invalid conversion from runtime type String to Integer

The four situations that produce it

Deserialised JSON. JSON.deserializeUntyped returns Object, and every value inside is an Object. A number in JSON arrives as Integer, Decimal or Double depending on how it was written, so casting to Integer works until someone sends 5.0.

Flow variables into invocable Apex. A flow passes values as generic types. An invocable method declaring List<String> and receiving IDs works. One declaring Integer and receiving a currency field does not.

A map of Object. Map<String, Object> is convenient and gives up all type safety. Every read out of it is a cast, and every cast is a potential runtime failure.

sObject downcasting. Casting a generic SObject to a specific type is fine when the record is that type and throws when a list contains a mixture, which happens more often than you would expect after a SOSL search. Casting a list element that turned out to be absent throws a different exception entirely.

The numeric case, in detail

This is the one that reaches production. JSON.deserializeUntyped('{"n": 5}') gives you an Integer. The same call with {"n": 5.0} gives you a Decimal. Casting blindly to either one fails half the time, and which half depends on the system sending the payload.

Object raw = ((Map<String, Object>) parsed).get('amount');
Decimal amount = raw == null ? null
    : raw instanceof Decimal ? (Decimal) raw
    : Decimal.valueOf(String.valueOf(raw));

Going through String.valueOf and back is deliberately blunt and it is reliable. The tidier answer is not to use deserializeUntyped at all.

The fix that removes the whole class of bug

Define an Apex class matching the payload and deserialise into it. The platform does the conversion, and a mismatch gives you a JSONException naming the field rather than a TypeException naming two types with no context.

public class OrderPayload {
    public String externalId;
    public Decimal amount;
    public List<LineItem> lines;
    public class LineItem { public String sku; public Integer qty; }
}

OrderPayload p = (OrderPayload) JSON.deserialize(body, OrderPayload.class);

You get compile-time types everywhere downstream, and the error, when it comes, says Cannot deserialize instance of int from VALUE_STRING value notanumber ... at [line:1, column:12], with the position in the payload. The full set of those messages is in JSON deserialization errors in Apex. That is a debuggable message.

Verified in a Summer ’26 scratch org on 14 September 2026: Integer.valueOf(9.87) returns 9 while Math.round(9.87) returns 10; String.valueOf(null) returns the four-character string null, not a null String. And instanceof against a null Object returns false. Note that testing instanceof on a variable already typed as the target class is a compile error, not a runtime false.

Use instanceof before casting, not after catching

Catching TypeException and carrying on is a smell. If a value can legitimately be two types, check:

if (record instanceof Account) {
    Account a = (Account) record;
}

instanceof on null returns false, which is convenient and occasionally surprising: a null value skips every branch silently rather than throwing.

Frequently asked

Why does it compile? Because a cast from Object is a promise you make to the compiler. It believes you and checks at runtime.

Is String.valueOf safe on any Object? Yes, including null, which returns the string “null” rather than a null String. That has caught people.

What about Integer.valueOf on a Decimal? It truncates rather than rounding, and it throws on a value beyond the Integer range. Math.round is usually what you meant.