# LLM-Specific Anti-Patterns in Apex
This guide documents systematic errors that LLMs (including Claude) commonly make when generating Salesforce Apex code. These patterns are critical to validate in generated code.
> **Source**: [LLM Mistakes in Apex & LWC - Salesforce Diaries](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)
---
## Table of Contents
1. [Non-Existent Methods](#1-non-existent-methods)
2. [Java Types Instead of Apex Types](#2-java-types-instead-of-apex-types)
3. [Map Access Without Null Safety](#3-map-access-without-null-safety)
4. [Missing SOQL Fields](#4-missing-soql-fields)
5. [Recursive Trigger Loops](#5-recursive-trigger-loops)
6. [Invalid InvocableVariable Types](#6-invalid-invocablevariable-types)
7. [Missing @JsonAccess Annotations](#7-missing-jsonaccess-annotations)
8. [Null Pointer from Missing Checks](#8-null-pointer-from-missing-checks)
9. [Incorrect DateTime Methods](#9-incorrect-datetime-methods)
10. [Collection Initialization Patterns](#10-collection-initialization-patterns)
---
## 1. Non-Existent Methods
LLMs often hallucinate methods that don't exist in Apex, borrowing syntax from Java or other languages.
### Common Hallucinated Methods
| Hallucinated Method | What LLM Expected | Correct Apex Alternative |
|---------------------|-------------------|--------------------------|
| `Datetime.addMilliseconds()` | Add milliseconds | `Datetime.addSeconds(ms/1000)` |
| `String.isEmpty(str)` | Static empty check | `String.isBlank(str)` |
| `List.stream()` | Java streams | Use `for` loops |
| `Map.getOrDefault()` | Default value | `map.get(key) ?? defaultValue` |
| `String.format()` | String formatting | `String.format()` exists but with different syntax |
| `Object.equals()` | Equality check | Use `==` or custom method |
| `List.sort(comparator)` | Custom sorting | Implement `Comparable` interface |
| `String.join(list)` | Join with delimiter | `String.join(list, delimiter)` |
### ❌ BAD: Hallucinated Datetime Method
```apex
// LLM generates this - DOES NOT EXIST
Datetime future = Datetime.now().addMilliseconds(500);
```
### ✅ GOOD: Correct Apex Pattern
```apex
// Apex has no millisecond precision - use seconds
Datetime future = Datetime.now().addSeconds(1);
// For sub-second timing, use System.currentTimeMillis()
Long startMs = System.currentTimeMillis();
// ... operation ...
Long elapsedMs = System.currentTimeMillis() - startMs;
```
### ❌ BAD: Java Stream Syntax
```apex
// LLM generates this - Java streams don't exist in Apex
List names = accounts.stream()
.map(a -> a.Name)
.collect(Collectors.toList());
```
### ✅ GOOD: Apex Loop Pattern
```apex
// Use traditional loops in Apex
List names = new List();
for (Account a : accounts) {
names.add(a.Name);
}
```
---
## 2. Java Types Instead of Apex Types
LLMs trained on Java code often use Java collection types that don't exist in Apex.
### Java Types to Avoid
| Java Type | Apex Equivalent |
|-----------|-----------------|
| `ArrayList` | `List` |
| `HashMap` | `Map` |
| `HashSet` | `Set` |
| `StringBuffer` | `String` (immutable) or `List` + `String.join()` |
| `StringBuilder` | `String` (immutable) or `List` + `String.join()` |
| `LinkedList` | `List` |
| `TreeMap` | `Map` (no ordering guarantee) |
| `Vector` | `List` |
| `Hashtable` | `Map` |
### ❌ BAD: Java Collection Types
```apex
// LLM generates these - COMPILE ERROR
ArrayList accounts = new ArrayList();
HashMap contactMap = new HashMap();
StringBuilder sb = new StringBuilder();
```
### ✅ GOOD: Apex Native Types
```apex
// Apex uses these types
List accounts = new List();
Map contactMap = new Map();
// For string concatenation
String result = '';
for (String s : parts) {
result += s; // OK for small strings
}
// For large string building
List parts = new List();
parts.add('Part 1');
parts.add('Part 2');
String result = String.join(parts, '');
```
### Detection Rule
```
REGEX: \b(ArrayList|HashMap|HashSet|StringBuffer|StringBuilder|LinkedList|TreeMap|Vector|Hashtable)\s*<
SEVERITY: CRITICAL
MESSAGE: Java type "{match}" does not exist in Apex. Use Apex native collections.
```
---
## 3. Map Access Without Null Safety
LLMs often use `Map.get()` without checking if the key exists, causing null pointer exceptions.
### ❌ BAD: Unsafe Map Access
```apex
Map accountMap = new Map([SELECT Id, Name FROM Account]);
for (Contact c : contacts) {
// DANGER: If AccountId not in map, .Name throws NPE
String accountName = accountMap.get(c.AccountId).Name;
}
```
### ✅ GOOD: Safe Map Access (Option 1 - containsKey)
```apex
Map accountMap = new Map([SELECT Id, Name FROM Account]);
for (Contact c : contacts) {
if (accountMap.containsKey(c.AccountId)) {
String accountName = accountMap.get(c.AccountId).Name;
}
}
```
### ✅ GOOD: Safe Map Access (Option 2 - Null Check)
```apex
Map accountMap = new Map([SELECT Id, Name FROM Account]);
for (Contact c : contacts) {
Account acc = accountMap.get(c.AccountId);
if (acc != null) {
String accountName = acc.Name;
}
}
```
### ✅ GOOD: Safe Map Access (Option 3 - Safe Navigation)
```apex
Map accountMap = new Map([SELECT Id, Name FROM Account]);
for (Contact c : contacts) {
// Safe navigation operator (?.) - returns null if key not found
String accountName = accountMap.get(c.AccountId)?.Name;
if (accountName != null) {
// Process
}
}
```
### ✅ GOOD: Safe Map Access (Option 4 - Null Coalescing)
```apex
Map accountMap = new Map([SELECT Id, Name FROM Account]);
for (Contact c : contacts) {
// Null coalescing operator (??) for default values
String accountName = accountMap.get(c.AccountId)?.Name ?? 'Unknown';
}
```
---
## 4. Missing SOQL Fields
LLMs often query fields but then access different fields not in the SELECT clause.
### ❌ BAD: Accessing Unqueried Fields
```apex
// Only querying Id and Name
List accounts = [SELECT Id, Name FROM Account];
for (Account acc : accounts) {
// RUNTIME ERROR: Industry was not queried!
if (acc.Industry == 'Technology') {
acc.Description = 'Tech company'; // Description also not queried!
}
}
```
### ✅ GOOD: Query All Accessed Fields
```apex
// Query all fields that will be accessed
List accounts = [SELECT Id, Name, Industry, Description FROM Account];
for (Account acc : accounts) {
if (acc.Industry == 'Technology') {
acc.Description = 'Tech company';
}
}
```
### ❌ BAD: Missing Relationship Fields
```apex
// Missing Account.Name in query
List contacts = [SELECT Id, Name, AccountId FROM Contact];
for (Contact c : contacts) {
// RUNTIME ERROR: Account.Name not queried
System.debug('Account: ' + c.Account.Name);
}
```
### ✅ GOOD: Include Relationship Fields
```apex
// Include relationship fields using dot notation
List contacts = [SELECT Id, Name, AccountId, Account.Name FROM Contact];
for (Contact c : contacts) {
System.debug('Account: ' + c.Account.Name); // Works!
}
```
### Validation Checklist
Before running code, verify:
1. All fields accessed in `if` statements are queried
2. All fields accessed in assignments are queried
3. All relationship fields (e.g., `Account.Name`) are in SELECT
4. Parent relationship uses `.` notation in query (e.g., `Contact.Account.Name`)
---
## 5. Recursive Trigger Loops
LLMs often forget to add recursion prevention in triggers, causing infinite loops.
### ❌ BAD: No Recursion Prevention
```apex
// Trigger that updates related records
trigger AccountTrigger on Account (after update) {
List contactsToUpdate = new List();
for (Account acc : Trigger.new) {
// This update might trigger another trigger, which might update Account...
for (Contact c : [SELECT Id FROM Contact WHERE AccountId = :acc.Id]) {
c.MailingCity = acc.BillingCity;
contactsToUpdate.add(c);
}
}
update contactsToUpdate; // Could cause recursion!
}
```
### ✅ GOOD: Static Flag Pattern
```apex
// TriggerHelper.cls
public class TriggerHelper {
private static Boolean isFirstRun = true;
public static Boolean isFirstRun() {
if (isFirstRun) {
isFirstRun = false;
return true;
}
return false;
}
public static void reset() {
isFirstRun = true;
}
}
// AccountTrigger.trigger
trigger AccountTrigger on Account (after update) {
if (!TriggerHelper.isFirstRun()) {
return; // Skip on recursive calls
}
// ... trigger logic ...
}
```
### ✅ BETTER: Set-Based Recursion Control
```apex
// TriggerRecursionHandler.cls
public class TriggerRecursionHandler {
private static Set processedIds = new Set();
public static Boolean hasProcessed(Id recordId) {
return processedIds.contains(recordId);
}
public static void markProcessed(Id recordId) {
processedIds.add(recordId);
}
public static void markProcessed(Set recordIds) {
processedIds.addAll(recordIds);
}
}
// In trigger handler
for (Account acc : Trigger.new) {
if (TriggerRecursionHandler.hasProcessed(acc.Id)) {
continue;
}
TriggerRecursionHandler.markProcessed(acc.Id);
// Process record...
}
```
### ✅ BEST: Trigger Actions Framework
```apex
// Use TAF for built-in recursion control via metadata
// See: references/trigger-actions-framework.md
```
---
## 6. Invalid InvocableVariable Types
LLMs often use unsupported types in `@InvocableVariable` annotations for Flow/Process Builder integration.
### Supported InvocableVariable Types
| Category | Supported Types |
|----------|----------------|
| **Primitives** | `Boolean`, `Date`, `DateTime`, `Decimal`, `Double`, `Id`, `Integer`, `Long`, `String`, `Time` |
| **Collections** | `List` where T is a supported type |
| **sObjects** | Any standard or custom sObject |
| **Apex-Defined** | Classes with `@InvocableVariable` on fields |
### Unsupported Types
| Type | Why Unsupported | Alternative |
|------|----------------|-------------|
| `Map` | Not serializable to Flow | Use List of wrapper class |
| `Set` | Not serializable to Flow | Use `List` |
| `Object` | Too generic | Use specific type |
| `Blob` | Not serializable | Use `String` (Base64) |
| Custom classes without `@InvocableVariable` | Not marked for Flow | Add annotations |
### ❌ BAD: Unsupported InvocableVariable Types
```apex
public class FlowInput {
@InvocableVariable
public Map options; // NOT SUPPORTED!
@InvocableVariable
public Set recordIds; // NOT SUPPORTED!
@InvocableVariable
public Blob fileContent; // NOT SUPPORTED!
}
```
### ✅ GOOD: Supported Types with Wrapper Pattern
```apex
public class FlowInput {
@InvocableVariable(label='Record IDs' description='Comma-separated IDs')
public List recordIds; // List is supported
@InvocableVariable(label='Options JSON' description='JSON string of options')
public String optionsJson; // Serialize Map to JSON string
@InvocableVariable(label='File Content' description='Base64 encoded content')
public String fileContentBase64; // Base64 encode Blob
}
// In your method, deserialize as needed
public static void process(List inputs) {
for (FlowInput input : inputs) {
Map options = (Map)JSON.deserialize(
input.optionsJson,
Map.class
);
Blob fileContent = EncodingUtil.base64Decode(input.fileContentBase64);
}
}
```
---
## 7. Missing @JsonAccess Annotations
When using `JSON.serialize()` / `JSON.deserialize()` with inner classes or non-public classes, LLMs forget the `@JsonAccess` annotation required since API version 49.0.
### ❌ BAD: Missing JsonAccess
```apex
public class AccountService {
// Inner class without @JsonAccess - JSON.serialize() fails silently
private class AccountWrapper {
public String name;
public String industry;
}
public static String getAccountsJson() {
List wrappers = new List();
// ... populate ...
return JSON.serialize(wrappers); // Returns "[]" or throws error!
}
}
```
### ✅ GOOD: With JsonAccess Annotation
```apex
public class AccountService {
@JsonAccess(serializable='always' deserializable='always')
private class AccountWrapper {
public String name;
public String industry;
}
public static String getAccountsJson() {
List wrappers = new List();
// ... populate ...
return JSON.serialize(wrappers); // Works correctly!
}
}
```
### When @JsonAccess is Required
| Class Type | Needs @JsonAccess? |
|------------|-------------------|
| Public top-level class | No |
| Private inner class | **Yes** |
| Protected inner class | **Yes** |
| Public inner class | No (but recommended for clarity) |
| Class used only internally | No (unless serialized) |
---
## 8. Null Pointer from Missing Checks
LLMs often chain method calls without null safety, leading to null pointer exceptions.
### ❌ BAD: Chained Calls Without Null Checks
```apex
// Any of these could be null!
String city = [SELECT Id, Account.BillingAddress FROM Contact LIMIT 1]
.Account
.BillingAddress
.getCity();
```
### ✅ GOOD: Safe Navigation Operator
```apex
// Use ?. for safe navigation
Contact c = [SELECT Id, Account.BillingCity FROM Contact LIMIT 1];
String city = c?.Account?.BillingCity;
// With default value
String city = c?.Account?.BillingCity ?? 'Unknown';
```
### ✅ GOOD: Explicit Null Checks
```apex
Contact c = [SELECT Id, Account.BillingCity FROM Contact LIMIT 1];
String city = 'Unknown';
if (c != null && c.Account != null && c.Account.BillingCity != null) {
city = c.Account.BillingCity;
}
```
---
## 9. Incorrect DateTime Methods
LLMs confuse Date and DateTime methods, which have different APIs.
### Date vs DateTime Method Confusion
| Operation | Date Method | DateTime Method |
|-----------|-------------|-----------------|
| Add days | `addDays(n)` | `addDays(n)` |
| Add months | `addMonths(n)` | `addMonths(n)` |
| Add years | `addYears(n)` | N/A (use `addMonths(n*12)`) |
| Add hours | N/A | `addHours(n)` |
| Add minutes | N/A | `addMinutes(n)` |
| Add seconds | N/A | `addSeconds(n)` |
| Get day | `day()` | `day()` |
| Get month | `month()` | `month()` |
| Get year | `year()` | `year()` |
| Get hour | N/A | `hour()` |
| Get minute | N/A | `minute()` |
| Today | `Date.today()` | N/A |
| Now | N/A | `DateTime.now()` |
### ❌ BAD: Mixing Date/DateTime Methods
```apex
// Date doesn't have addHours!
Date d = Date.today();
Date future = d.addHours(5); // COMPILE ERROR
// DateTime doesn't have a static today()!
DateTime now = DateTime.today(); // COMPILE ERROR
```
### ✅ GOOD: Correct Method Usage
```apex
// For Date operations
Date d = Date.today();
Date future = d.addDays(5);
// For DateTime operations
DateTime now = DateTime.now();
DateTime future = now.addHours(5);
// Converting between Date and DateTime
Date d = Date.today();
DateTime dt = DateTime.newInstance(d, Time.newInstance(0, 0, 0, 0));
DateTime dt = DateTime.now();
Date d = dt.date();
```
---
## 10. Collection Initialization Patterns
LLMs sometimes use incorrect patterns for initializing collections from SOQL or other collections.
### ❌ BAD: Incorrect Map Initialization
```apex
// This doesn't work - can't construct Map from SOQL directly with fields
Map nameMap = new Map(
[SELECT Id, Name FROM Account]
); // COMPILE ERROR - wrong constructor
```
### ✅ GOOD: Correct Map Initialization
```apex
// Map works directly with SOQL
Map accountMap = new Map(
[SELECT Id, Name FROM Account]
);
// For Map, use a loop
Map nameMap = new Map();
for (Account acc : [SELECT Id, Name FROM Account]) {
nameMap.put(acc.Id, acc.Name);
}
```
### ❌ BAD: List to Set Conversion
```apex
// Can't directly convert List to Set in constructor
List idList = new List{'001...', '001...'};
Set idSet = new Set(idList); // Actually this DOES work in Apex!
```
### ✅ GOOD: Collection Conversions
```apex
// List to Set - this works!
List idList = new List{'001...', '001...'};
Set idSet = new Set(idList);
// Set to List
Set idSet = new Set{'001...', '001...'};
List idList = new List(idSet);
// Map keys to Set
Map accountMap = new Map([SELECT Id FROM Account]);
Set accountIds = accountMap.keySet();
// Map values to List
List accounts = accountMap.values();
```
---
## Quick Reference: LLM Validation Checklist
Before accepting LLM-generated Apex code, verify:
### Methods & Types
- [ ] No Java collection types (ArrayList, HashMap, etc.)
- [ ] No hallucinated methods (addMilliseconds, stream(), etc.)
- [ ] Correct Date vs DateTime methods
### Null Safety
- [ ] Map.get() has null check or uses containsKey()
- [ ] Chained method calls use safe navigation (?.)
- [ ] SOQL results checked before accessing
### SOQL
- [ ] All accessed fields are in SELECT clause
- [ ] Relationship fields use dot notation in query
- [ ] Parent records accessed safely
### Flow Integration
- [ ] InvocableVariable uses supported types
- [ ] No Map/Set in @InvocableVariable
- [ ] @JsonAccess on inner classes if serialized
### Triggers
- [ ] Recursion prevention mechanism in place
- [ ] Static flag or processed ID tracking
---
## Detection Script
Add this validation to your CI/CD pipeline:
```python
# detect_llm_patterns.py
import re
JAVA_TYPES = [
r'\bArrayList\s*<',
r'\bHashMap\s*<',
r'\bHashSet\s*<',
r'\bStringBuffer\b',
r'\bStringBuilder\b',
r'\bLinkedList\s*<',
r'\bTreeMap\s*<',
]
HALLUCINATED_METHODS = [
r'\.addMilliseconds\s*\(',
r'\.stream\s*\(\)',
r'\.getOrDefault\s*\(',
r'DateTime\.today\s*\(\)',
]
def validate_apex(content):
issues = []
for pattern in JAVA_TYPES:
if re.search(pattern, content):
issues.append(f"Java type detected: {pattern}")
for pattern in HALLUCINATED_METHODS:
if re.search(pattern, content):
issues.append(f"Non-existent method: {pattern}")
return issues
```
---
## Reference
- **Existing Anti-Patterns**: See `references/anti-patterns.md` for traditional Apex anti-patterns
- **Best Practices**: See `references/best-practices.md` for correct patterns
- **Source**: [Salesforce Diaries - LLM Mistakes](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)