# 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/)