Apex Salesforce Id Patterns
Activate this skill when Apex must reason about a Salesforce Id — validating it came from a real record, determining which sObject it points to, or comparing Ids across the 15/18-character boundary. It covers the difference between String and Id types, the prefix-based describe lookup, and case sensitivity of 15-character Ids.
Before Starting
Gather this context before working on anything in this domain:
- Where does the Id come from? User input, external system, URL parameter, SOQL result, or trigger context?
- Is the Id expected to be one specific sObject, or could it be any of several?
- Do you need the record, or just the sObject type? Type detection is cheap (no SOQL). Fetching the record is not.
- Are both 15-char (case-sensitive) and 18-char (case-insensitive) versions potentially in play?
Core Concepts
15 vs 18 Character Ids
- 15-char Ids are case-sensitive.
005A000000ABCdE and 005A000000abcde are different records.
- 18-char Ids append a 3-character checksum that makes them case-insensitive safe to compare as strings.
- The Apex
Id type normalizes to 18 characters on assignment. Comparing Id == Id always works.
- Comparing strings across the boundary (e.g., a 15-char URL param to an 18-char SOQL result) will return
false without warning.
Apex Id Is A Typed Value, Not Just A String
Id is a primitive type. Assigning an invalid string to Id throws System.StringException.
Id.valueOf(str) validates format and sobject-type legitimacy — it throws if the prefix is not a known sObject.
- Prefer
Id typing over String in method signatures when the parameter must be a Salesforce Id.
Detecting sObject Type From An Id
Two mechanisms, same result:
myId.getSobjectType() — returns the Schema.SObjectType for a typed Id. Cheap, no SOQL.
Schema.getGlobalDescribe().get(myId.substring(0,3)) — prefix lookup, but not all prefixes are unique (some managed packages overlap, and custom objects share the a0* range).
Rule: always prefer getSobjectType() on a typed Id. Fall back to prefix only if you have a String you cannot type yet.
Key Prefixes To Recognize
| Prefix |
sObject |
001 |
Account |
003 |
Contact |
005 |
User |
006 |
Opportunity |
00Q |
Lead |
500 |
Case |
a0x–a0z, a0* |
Custom objects (prefix is org-specific) |
00D |
Organization |
0F0 |
Folder |
Do NOT hardcode custom object prefixes. They are org-specific.
Common Patterns
Validate A User-Supplied Id Before Using It
When to use: A controller receives an Id from a URL, Experience Cloud form, or external integration.
How it works:
public static Account fetchAccount(String rawId) {
Id accountId;
try {
accountId = (Id) rawId;
} catch (System.StringException e) {
throw new AuraHandledException('Invalid Id format.');
}
if (accountId.getSobjectType() != Account.SObjectType) {
throw new AuraHandledException('Id does not belong to an Account.');
}
return [SELECT Id, Name FROM Account WHERE Id = :accountId WITH USER_MODE LIMIT 1];
}
Why not the alternative: Querying directly with an unvalidated string either throws QueryException (wrong prefix) or a less helpful StringException with no caller guidance.
Detect Type Across A Heterogeneous List Of Ids
When to use: An Apex method accepts List<Id> where entries may belong to different sObjects.
How it works:
Map<Schema.SObjectType, List<Id>> byType = new Map<Schema.SObjectType, List<Id>>();
for (Id idValue : inputIds) {
Schema.SObjectType type = idValue.getSobjectType();
if (!byType.containsKey(type)) byType.put(type, new List<Id>());
byType.get(type).add(idValue);
}
// Issue one SOQL per type instead of per record.
Normalize 15-Char Id To 18-Char For String Compare
When to use: Comparing an Id from an external system or CSV (often 15-char) with an Id from SOQL (always 18-char).
How it works: Cast through Id: Id normalized = (Id) fifteenCharString; — the typed Id is 18 chars and case-insensitive-safe for string compare.
Decision Guidance
| Situation |
Recommended Approach |
Reason |
| Comparing two Ids you own |
Id == Id |
Apex normalizes to 18 chars automatically |
| Comparing strings from mixed sources |
Cast both to Id first |
15 vs 18 char mismatch is silent |
| Detecting sObject from a typed Id |
id.getSobjectType() |
No SOQL, no describe overhead |
| Detecting sObject from a string when you cannot trust the prefix |
Id.valueOf(str).getSobjectType() |
Throws if not a legal Id |
| Validating external-system Ids |
try { (Id) str; } catch ... |
Give a helpful error, not a raw exception |
| Routing logic by object |
Switch on Schema.SObjectType |
Safer than hardcoded prefixes |
Recommended Workflow
- Identify every point the Id crosses a trust boundary — URL, DTO, CSV, external API response.
- Type the parameter as
Id, not String, everywhere possible.
- Where the source is untrusted, wrap the cast in a
try/catch and surface a clear error.
- Before SOQL, confirm sObject with
getSobjectType() (not a string prefix compare).
- Add a negative test: pass an invalid string, an Id for the wrong object, and an empty string; each should fail cleanly.
Review Checklist
Salesforce-Specific Gotchas
- 15-char strings do not equal 18-char strings — always cast to
Id before a string compare.
- Custom object prefixes are org-specific —
a03 in one org is a different object in another.
Id.valueOf(null) throws — guard null separately.
- Some managed-package prefixes collide with standard orgs — always prefer
getSobjectType() over prefix lookup.
- Trigger
oldMap keys are typed Id — iterating as String loses the type info.
System.StringException from (Id) someString is the clue that a caller passed garbage; handle it with intent.
Id cannot be deserialized from JSON with a typo — JSON.deserialize silently becomes null on invalid strings, not throw.
Output Artifacts
| Artifact |
Description |
scripts/check_apex_salesforce_id_patterns.py |
Scans for string-prefix Id checks, 15/18 char string compares, and untyped Id parameters |
templates/apex-salesforce-id-patterns-template.md |
Work template for validating and typing Id inputs at trust boundaries |
Related Skills
apex-user-and-permission-checks — authorization once an Id has been validated
apex-with-user-mode — enforcing FLS/CRUD on the SOQL that consumes the validated Id
apex-bulk-patterns — when the Id is part of a bulkified collection
1---2name: apex-salesforce-id-patterns3description: Use when working with Salesforce Ids in Apex — validating Id format, detecting the target sObject type from a string Id, or safely handling 15 vs 18-character Ids. Trigger keywords: Id prefix, Id.valueOf, Id.getSobjectType, 15-char, 18-char, case-insensitive Id. NOT for hardcoded Profile or RecordType Ids — use apex/apex-hardcoded-id-elimination. NOT for bulk Id collection patterns — use apex/apex-collections-patterns.4---56# Apex Salesforce Id Patterns78Activate this skill when Apex must reason about a Salesforce Id — validating it came from a real record, determining which sObject it points to, or comparing Ids across the 15/18-character boundary. It covers the difference between `String` and `Id` types, the prefix-based describe lookup, and case sensitivity of 15-character Ids.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- Where does the Id come from? User input, external system, URL parameter, SOQL result, or trigger context?17- Is the Id expected to be one specific sObject, or could it be any of several?18- Do you need the **record**, or just the **sObject type**? Type detection is cheap (no SOQL). Fetching the record is not.19- Are both 15-char (case-sensitive) and 18-char (case-insensitive) versions potentially in play?2021---2223## Core Concepts2425### 15 vs 18 Character Ids2627- 15-char Ids are **case-sensitive**. `005A000000ABCdE` and `005A000000abcde` are different records.28- 18-char Ids append a 3-character checksum that makes them **case-insensitive** safe to compare as strings.29- The Apex `Id` type **normalizes to 18 characters** on assignment. Comparing `Id == Id` always works.30- Comparing **strings** across the boundary (e.g., a 15-char URL param to an 18-char SOQL result) will return `false` without warning.3132### Apex `Id` Is A Typed Value, Not Just A String3334- `Id` is a primitive type. Assigning an invalid string to `Id` throws `System.StringException`.35- `Id.valueOf(str)` validates format and sobject-type legitimacy — it throws if the prefix is not a known sObject.36- Prefer `Id` typing over `String` in method signatures when the parameter must be a Salesforce Id.3738### Detecting sObject Type From An Id3940Two mechanisms, same result:41421. `myId.getSobjectType()` — returns the `Schema.SObjectType` for a typed `Id`. Cheap, no SOQL.432. `Schema.getGlobalDescribe().get(myId.substring(0,3))` — prefix lookup, but not all prefixes are unique (some managed packages overlap, and custom objects share the `a0*` range).4445Rule: always prefer `getSobjectType()` on a typed `Id`. Fall back to prefix only if you have a `String` you cannot type yet.4647### Key Prefixes To Recognize4849| Prefix | sObject |50|---|---|51| `001` | Account |52| `003` | Contact |53| `005` | User |54| `006` | Opportunity |55| `00Q` | Lead |56| `500` | Case |57| `a0x–a0z, a0*` | Custom objects (prefix is org-specific) |58| `00D` | Organization |59| `0F0` | Folder |6061Do NOT hardcode custom object prefixes. They are org-specific.6263---6465## Common Patterns6667### Validate A User-Supplied Id Before Using It6869**When to use:** A controller receives an `Id` from a URL, Experience Cloud form, or external integration.7071**How it works:**7273```apex74public static Account fetchAccount(String rawId) {75 Id accountId;76 try {77 accountId = (Id) rawId;78 } catch (System.StringException e) {79 throw new AuraHandledException('Invalid Id format.');80 }81 if (accountId.getSobjectType() != Account.SObjectType) {82 throw new AuraHandledException('Id does not belong to an Account.');83 }84 return [SELECT Id, Name FROM Account WHERE Id = :accountId WITH USER_MODE LIMIT 1];85}86```8788**Why not the alternative:** Querying directly with an unvalidated string either throws `QueryException` (wrong prefix) or a less helpful `StringException` with no caller guidance.8990### Detect Type Across A Heterogeneous List Of Ids9192**When to use:** An Apex method accepts `List<Id>` where entries may belong to different sObjects.9394**How it works:**9596```apex97Map<Schema.SObjectType, List<Id>> byType = new Map<Schema.SObjectType, List<Id>>();98for (Id idValue : inputIds) {99 Schema.SObjectType type = idValue.getSobjectType();100 if (!byType.containsKey(type)) byType.put(type, new List<Id>());101 byType.get(type).add(idValue);102}103// Issue one SOQL per type instead of per record.104```105106### Normalize 15-Char Id To 18-Char For String Compare107108**When to use:** Comparing an Id from an external system or CSV (often 15-char) with an Id from SOQL (always 18-char).109110**How it works:** Cast through `Id`: `Id normalized = (Id) fifteenCharString;` — the typed Id is 18 chars and case-insensitive-safe for string compare.111112---113114## Decision Guidance115116| Situation | Recommended Approach | Reason |117|---|---|---|118| Comparing two Ids you own | `Id == Id` | Apex normalizes to 18 chars automatically |119| Comparing strings from mixed sources | Cast both to `Id` first | 15 vs 18 char mismatch is silent |120| Detecting sObject from a typed Id | `id.getSobjectType()` | No SOQL, no describe overhead |121| Detecting sObject from a string when you cannot trust the prefix | `Id.valueOf(str).getSobjectType()` | Throws if not a legal Id |122| Validating external-system Ids | `try { (Id) str; } catch ...` | Give a helpful error, not a raw exception |123| Routing logic by object | Switch on `Schema.SObjectType` | Safer than hardcoded prefixes |124125---126127## Recommended Workflow1281291. Identify every point the Id crosses a trust boundary — URL, DTO, CSV, external API response.1302. Type the parameter as `Id`, not `String`, everywhere possible.1313. Where the source is untrusted, wrap the cast in a `try/catch` and surface a clear error.1324. Before SOQL, confirm sObject with `getSobjectType()` (not a string prefix compare).1335. Add a negative test: pass an invalid string, an Id for the wrong object, and an empty string; each should fail cleanly.134135---136137## Review Checklist138139- [ ] No hardcoded 3-character prefixes for custom objects.140- [ ] No `String.startsWith('001')` style type detection where an `Id` is available.141- [ ] All untrusted Id inputs are cast through `Id` or `Id.valueOf` with caught `StringException`.142- [ ] SOQL bind variables typed as `Id` or `Set<Id>`, not `String`.143- [ ] Tests cover: invalid format, wrong-type Id, empty/null.144145---146147## Salesforce-Specific Gotchas1481491. **15-char strings do not equal 18-char strings** — always cast to `Id` before a string compare.1502. **Custom object prefixes are org-specific** — `a03` in one org is a different object in another.1513. **`Id.valueOf(null)` throws** — guard null separately.1524. **Some managed-package prefixes collide** with standard orgs — always prefer `getSobjectType()` over prefix lookup.1535. **Trigger `oldMap` keys are typed `Id`** — iterating as `String` loses the type info.1546. **`System.StringException` from `(Id) someString`** is the clue that a caller passed garbage; handle it with intent.1557. **`Id` cannot be deserialized from JSON with a typo** — `JSON.deserialize` silently becomes null on invalid strings, not throw.156157---158159## Output Artifacts160161| Artifact | Description |162|---|---|163| `scripts/check_apex_salesforce_id_patterns.py` | Scans for string-prefix Id checks, 15/18 char string compares, and untyped Id parameters |164| `templates/apex-salesforce-id-patterns-template.md` | Work template for validating and typing Id inputs at trust boundaries |165166---167168## Related Skills169170- `apex-user-and-permission-checks` — authorization once an Id has been validated171- `apex-with-user-mode` — enforcing FLS/CRUD on the SOQL that consumes the validated Id172- `apex-bulk-patterns` — when the Id is part of a bulkified collection