SfSkills Domains Docs GitHub

Home / apex / apex-string-and-regex

apex-string-and-regex

Apex String class methods, Pattern/Matcher regex, text parsing, template rendering, and null-safety landmines. 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 catastrophic backtracking or the 1M char regex cap — use apex/apex-regex-and-pattern-matching. NOT for SOQL literal escaping — use apex/soql-string-escaping-and-reserved-characters.

Open the skill folder on GitHub


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


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.

MethodNull 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 nullReturns 'null' (the four-character string), not actual null
s.split(regex) where s is nullNullPointerException
String.escapeSingleQuotes(s) where s is nullReturns 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:

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.

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

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()):

Matcher m = Pattern.compile('(\\d+)').matcher(input);
if (m.find()) {
    String captured = m.group(1);
}

Common Patterns

Pattern A — Validate the entire input

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

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

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

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

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

TaskUse thisReason
"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

  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


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

ArtifactDescription
Refactored helper classString/regex helpers with static final Patterns and null-safe guards
Test classCoverage for null, empty, validation-pass, validation-fail, extraction-empty, extraction-multi cases
Code-review notesPer call-site: which checklist item motivated the change

Package files