apex-string-and-regex
Apex String class methods, Pattern/Matcher regex, text parsing, template rendering, and the null-safety landmines that bite every Apex programmer eventually. Covers `String.split` trailing-empty-drop semantics, `Pattern.compile` static-final caching, `Matcher.group` ordering rules, and the `String.format` MessageFormat-vs-printf trap. NOT for formula functions (FORMULA / TEXT in formula fields), NOT for SOQL string-bind escaping (see apex/dynamic-soql).
Best use case
apex-string-and-regex is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Apex String class methods, Pattern/Matcher regex, text parsing, template rendering, and the null-safety landmines that bite every Apex programmer eventually. Covers `String.split` trailing-empty-drop semantics, `Pattern.compile` static-final caching, `Matcher.group` ordering rules, and the `String.format` MessageFormat-vs-printf trap. NOT for formula functions (FORMULA / TEXT in formula fields), NOT for SOQL string-bind escaping (see apex/dynamic-soql).
Teams using apex-string-and-regex should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/apex-string-and-regex/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How apex-string-and-regex Compares
| Feature / Agent | apex-string-and-regex | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Apex String class methods, Pattern/Matcher regex, text parsing, template rendering, and the null-safety landmines that bite every Apex programmer eventually. Covers `String.split` trailing-empty-drop semantics, `Pattern.compile` static-final caching, `Matcher.group` ordering rules, and the `String.format` MessageFormat-vs-printf trap. NOT for formula functions (FORMULA / TEXT in formula fields), NOT for SOQL string-bind escaping (see apex/dynamic-soql).
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Apex String and Regex
The Apex String class plus `Pattern` / `Matcher` is the everyday text-handling
toolkit in Salesforce. Most Apex programmers learn it through error messages —
`NullPointerException` on a method call, `System.StringException` from an
`m.group()` with no preceding `find()`, a `String.split` result that's missing
the last element, a `String.format` that prints `%s` literally. This skill
encodes the boundary rules and the right-shape-first patterns so those errors
stop happening.
What this skill is NOT. Formula-language `TEXT()` / `FORMULA()` is a different
runtime — go to the formula reference. SOQL string-binding (preventing
injection in dynamic queries) lives in `apex/dynamic-soql` because the
escaping rules are SOQL-specific, not String-class-specific.
---
## Before Starting
- Identify whether the input string can be **null**. Assume yes unless you
can name the upstream check that already enforced non-null.
- Identify whether the call is on the **hot path** (loop body, trigger,
per-record callout). If yes, any `Pattern.compile(...)` belongs at class
scope as `static final`, not inline.
- Identify whether you want the **first match** or **every match**. Apex's
default `Matcher.matches()` is *full-string match*, not "find a match
anywhere" — that's `find()`. Choosing wrong is the most common regex bug.
---
## Core Concepts
### Null-safety: which methods crash, which return null, which return false
Apex's String class has three different null-handling shapes; mixing them is
the source of half the production bugs.
| Method | Null input behavior |
|---|---|
| `s.length()`, `s.toLowerCase()`, `s.contains(...)`, etc. (instance methods) | **NullPointerException** — `s` is null |
| `String.isBlank(s)`, `String.isNotBlank(s)`, `String.isEmpty(s)` | **Null-safe** — returns boolean |
| `String.valueOf(o)` where `o` is null | **Returns 'null'** (the four-character string), not actual null |
| `s.split(regex)` where `s` is null | **NullPointerException** |
| `String.escapeSingleQuotes(s)` where `s` is null | **Returns null** (does not crash) |
The right pattern: gate every instance-method call on a `String.isNotBlank`
check, or use the safe-navigation operator `?.` if you only need the call's
result and `null` is acceptable.
### Pattern compilation cost
`Pattern.compile(regex)` parses and builds a state machine. In a loop body
(trigger, batch handler) this dominates wall-clock if it runs per record.
The fix is at most three lines:
```apex
public class EmailValidator {
private static final Pattern EMAIL_RE =
Pattern.compile('^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$');
public static Boolean isValid(String s) {
if (String.isBlank(s)) return false;
return EMAIL_RE.matcher(s).matches();
}
}
```
`static final` means compiled once at class load, reused for every record.
### `matches()` vs `find()` — the regex bug everyone makes once
`Matcher.matches()` requires the **entire string** to match the regex.
`Matcher.find()` searches for **any substring** that matches.
```apex
Pattern p = Pattern.compile('foo');
Matcher m1 = p.matcher('hello foo world');
m1.matches(); // false — entire string is not "foo"
m1.find(); // true — substring "foo" found
Matcher m2 = p.matcher('foo');
m2.matches(); // true — entire string is "foo"
m2.find(); // true
```
Use `matches()` for validation (whole input must match). Use `find()` (often
in a `while (m.find())` loop) for extraction.
### `Matcher.group()` requires a successful match first
```apex
Matcher m = Pattern.compile('(\\d+)').matcher('abc');
String g = m.group(1); // throws System.StringException
```
The fix is always to gate `group()` on a successful `find()` (or `matches()`):
```apex
Matcher m = Pattern.compile('(\\d+)').matcher(input);
if (m.find()) {
String captured = m.group(1);
}
```
---
## Common Patterns
### Pattern A — Validate the entire input
```apex
private static final Pattern PHONE_RE = Pattern.compile('^\\+?[0-9 ()-]{7,20}$');
public static Boolean isValidPhone(String s) {
if (String.isBlank(s)) return false;
return PHONE_RE.matcher(s).matches();
}
```
`matches()`, not `find()`. Whole-input validation.
### Pattern B — Extract every occurrence
```apex
private static final Pattern URL_RE = Pattern.compile('https?://\\S+');
public static List<String> extractUrls(String body) {
List<String> out = new List<String>();
if (String.isBlank(body)) return out;
Matcher m = URL_RE.matcher(body);
while (m.find()) {
out.add(m.group());
}
return out;
}
```
`find()` in a loop. `m.group()` (no argument) returns the whole match;
`m.group(1)`, `m.group(2)` return the numbered capture groups.
### Pattern C — Template substitution with `String.format`
```apex
String greeting = String.format('Hello {0}, you have {1} new cases.',
new List<String>{ user.FirstName, String.valueOf(caseCount) });
```
**The trap.** Apex's `String.format` uses Java `MessageFormat` syntax
(`{0}`, `{1}`), **not** printf-style (`%s`, `%d`). If you pass `'Hello %s'`
the `%s` survives literally into the output.
The arguments must be a `List<String>`. Numbers get `String.valueOf()` first.
### Pattern D — Safe replace-all with backreferences
```apex
// Quote every word that starts with capital letter.
Pattern p = Pattern.compile('\\b([A-Z][a-z]+)\\b');
String quoted = p.matcher(input).replaceAll('"$1"');
```
`replaceAll('$1')` references the first capture group. Beware: `$` is a
special character — to use a literal `$` in the replacement, escape with
`\\$` or use `Matcher.quoteReplacement(literal)`.
### Pattern E — Split that doesn't drop trailing empty fields
```apex
// CSV row "a,b,,," — what we expect: 4 fields including 2 empty trailing
List<String> parts1 = 'a,b,,,'.split(','); // returns ['a', 'b'] — empties dropped
List<String> parts2 = 'a,b,,,'.split(',', -1); // returns ['a', 'b', '', '', '']
```
The two-arg form `split(regex, limit)` with `limit = -1` preserves trailing
empties. With `limit = 0` (the default) Java drops trailing empty strings.
This rule is documented but constantly forgotten.
---
## Decision Guidance
| Task | Use this | Reason |
|---|---|---|
| "Is this entire string a valid email?" | `Pattern.matcher(s).matches()` | matches = whole input; find = substring |
| "Pull every URL out of this body" | `while (m.find())` + `m.group()` | find = repeatable; group() returns the match |
| "Replace every digit with `*`" | `s.replaceAll('\\d', '*')` (String method, no Pattern needed) | Convenience method when the regex is one-shot |
| "Loop over 200 records, validating each" | `static final Pattern` + `matcher(s).matches()` | Avoid per-record `Pattern.compile` cost |
| "Build `'Hello {name}'` template" | `String.format('Hello {0}', new List<String>{ name })` | MessageFormat syntax, not printf |
| "Split and keep trailing empties" | `s.split(regex, -1)` | Java limit-0 drops trailing empties silently |
| "Null-check before any `.method()` call" | `String.isNotBlank(s)` | Null-safe; avoids NPE |
---
## Recommended Workflow
1. **Decide validation vs extraction.** Validation = whole-string match (`matches()`); extraction = `find()` in a loop. Wrong choice is the most common regex bug.
2. **Decide compile site.** Class-level `static final Pattern` if used more than once per request; inline `Pattern.compile` only for genuinely one-shot use.
3. **Add the null guard.** Every public entry point that takes a String parameter starts with `if (String.isBlank(s)) return ...;` (or the equivalent for your contract — false, null, empty list).
4. **Pick the right replace.** `s.replaceAll(regex, replacement)` for one-off replace; `Matcher.replaceAll` when you need backreferences and a pre-compiled Pattern.
5. **For templates: `String.format` with `{0}`, `{1}`.** Not `%s` — that's printf, which Apex doesn't use.
6. **Test the null and empty cases explicitly.** Don't rely on "the caller wouldn't pass null". Inspect the caller; if the caller could, your method must handle it.
---
## Review Checklist
- [ ] Every Pattern used in a loop is declared `static final` at class scope.
- [ ] Every `Matcher.group(...)` call is preceded by a successful `find()` or `matches()` in the same control-flow path.
- [ ] No `String.format(...)` uses `%s` / `%d`; all placeholders are `{0}`, `{1}`.
- [ ] Every public method accepting a String parameter null-guards its inputs.
- [ ] `s.split(regex, -1)` is used when trailing-empty fields matter.
- [ ] Replacement strings containing `$` are escaped (`Matcher.quoteReplacement(...)` or `\\$`).
- [ ] `matches()` vs `find()` choice matches the intent (validation vs extraction).
---
## Salesforce-Specific Gotchas
1. **`String.split(regex)` drops trailing empty strings.** Use `split(regex, -1)` to preserve them. (See `references/gotchas.md` § 1.)
2. **`String.format` uses MessageFormat (`{0}`), not printf (`%s`).** `%s` survives literally into the output. (See `references/gotchas.md` § 2.)
3. **Per-record `Pattern.compile()` is silently expensive.** Move to `static final`. (See `references/gotchas.md` § 3.)
4. **`Matcher.group()` without a prior successful `find()` / `matches()` throws `System.StringException`.** Always gate. (See `references/gotchas.md` § 4.)
5. **`String.valueOf(null)` returns the four-char string `'null'`, not actual null.** Don't compare its result with `null`. (See `references/gotchas.md` § 5.)
---
## Output Artifacts
| Artifact | Description |
|---|---|
| Refactored helper class | String/regex helpers with `static final` Patterns and null-safe guards |
| Test class | Coverage for null, empty, validation-pass, validation-fail, extraction-empty, extraction-multi cases |
| Code-review notes | Per call-site: which checklist item motivated the change |
---
## Related Skills
- `apex/dynamic-soql` — String escaping for SOQL injection-safe binds (different escape rules than text rendering)
- `apex/apex-mocking-and-stubs` — when the test class needs to verify regex behavior across edge cases
- `apex/trigger-framework` — when the regex usage lives inside a trigger handler; `static final Pattern` is mandatory thereRelated Skills
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
lwc-imperative-apex
Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).
dataweave-for-apex
Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).
flow-invocable-from-apex
Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).
flow-apex-defined-types
Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.
scheduled-apex-failure-detection-and-monitoring
Use when nightly batch / scheduled Apex jobs are failing without anyone noticing — covers why uncaught exceptions in `execute()` go to the debug log instead of email, how to query `AsyncApexJob` for `Status`, `NumberOfErrors`, and `ExtendedStatus`, when to implement `Database.RaisesPlatformEvents` so the platform publishes `BatchApexErrorEvent` on uncaught failures, how to subscribe to that event with an Apex trigger and notify operators, and how to layer a custom watcher schedule on top so silent-failure modes (job that never started, scheduled class deleted, queue stuck on `Queued`) still surface. Triggers: 'nightly batch failed at 2am with no notification', 'how do we know if a scheduled apex job is failing', 'BatchApexErrorEvent vs custom retry logic', 'Setup Apex Jobs only shows last 7 days, where else can I look', 'job is stuck in queued status nobody noticed for a week'. NOT for general Apex exception handling patterns (use apex/apex-exception-handling-and-logging), NOT for Batch Apex authoring or chunking strategy (use apex/batch-apex-design), NOT for Setup → Apex Jobs UI walkthrough as an admin task (use admin/batch-job-scheduling-and-monitoring), NOT for retry logic itself (use apex/scheduled-apex-retry-patterns once authored).
platform-events-apex
Use when publishing or subscribing to Salesforce Platform Events from Apex, comparing Platform Events with Change Data Capture, or designing event-triggered error handling and monitoring. Triggers: 'EventBus.publish', 'platform event trigger', 'CDC vs Platform Events', 'replay ID', 'high-volume event'. NOT for Flow-only publish/subscribe automation.
health-cloud-apex-extensions
Use this skill when extending Health Cloud via Apex: implementing HealthCloudGA managed-package interfaces, automating care plan lifecycle hooks, processing referrals using Industries Common Components invocable actions, or enforcing HIPAA-compliant logging governance for clinical Apex code. Trigger keywords: CarePlanProcessorCallback, HealthCloudGA namespace, ReferralRequest, ReferralResponse, care plan invocable actions, clinical Apex extension, Health Cloud Apex API. NOT for standard Apex triggers or generic Apex development unrelated to Health Cloud managed-package extension points.
fsl-apex-extensions
Use when writing Apex that calls Field Service Lightning scheduling APIs — AppointmentBookingService, ScheduleService, GradeSlotsService, or OAAS — to book, schedule, grade, or optimize service appointments programmatically. Trigger keywords: FSL Apex namespace, GetSlots, schedule service appointment via code, appointment booking API, FSL optimization API. NOT for standard Apex patterns unrelated to FSL, admin-level scheduling policy configuration, or declarative FSL scheduling.
fsc-apex-extensions
Use this skill when extending Financial Services Cloud (FSC) behavior through Apex: customizing financial rollup recalculation, disabling built-in FSC triggers to write custom trigger logic, implementing Compliant Data Sharing (CDS) participant/role integrations, or building custom FSC action handlers. NOT for standard Apex unrelated to the FSC managed package, standard Salesforce sharing rules, or configuring FSC rollups through the Admin UI — use the admin/financial-account-setup skill for declarative rollup setup.
entitlement-apex-hooks
Use this skill when writing Apex triggers or classes that interact with CaseMilestone records — completing milestones, detecting violations, or reacting to SLA state changes. Trigger keywords: CaseMilestone trigger, auto-complete milestone Apex, milestone violation polling, CompletionDate write pattern. NOT for entitlement process admin setup, milestone configuration in Setup UI, or Flow-based milestone actions.
dynamic-apex
Use when building or reviewing code that constructs SOQL/SOSL at runtime, inspects schema metadata via Schema.describe methods, accesses fields dynamically on sObjects, or performs runtime type inspection. Triggers: 'Database.query', 'Schema.getGlobalDescribe', 'Schema.describeSObjects', 'dynamic field access', 'SObjectType', 'DescribeFieldResult'. NOT for static SOQL queries or query performance tuning — use soql-fundamentals or soql-query-optimization.