SfSkills Domains Docs GitHub

Home / apex / apex-switch-on-sobject

apex-switch-on-sobject

Apex switch-on-SObjectType patterns — type dispatching across SObject collections, polymorphic handlers, the typed-variable binding that makes `when SObjectType varName` more than a tag check. Covers the single-type-per-when-block restriction, the null branch, the no-fall-through rule, and why `Type.forName()` cannot be used in a switch expression. NOT for `switch on` over an enum — use apex/apex-enum-patterns. NOT for plain Integer / String switch syntax (Apex Developer Guide). NOT for dynamic field access — use apex/dynamic-apex.

Open the skill folder on GitHub


Apex Switch on SObject

switch on over an SObject expression is the right tool for type-dispatch inside a heterogeneous collection — a generic trigger handler, a polymorphic processor, a classifier that runs different validation per object type. It buys two things over a chain of instanceof / getSObjectType().equals(...) checks: type-narrowed bindings inside each branch (no cast required) and a single explicit when else for the unhandled case.

What this skill is NOT. The simpler switch on Integer / String / enum form is plain Apex syntax — go directly to the Apex Developer Guide. Dynamic field access (Schema.getGlobalDescribe(), record.get(fieldName)) lives in apex/dynamic-apex. This skill is specifically about the SObject expression form and the branch-binding semantics.


Before Starting


Core Concepts

The two switch-on-SObject forms

// Form 1 — bind a typed variable inside the branch (the whole point of this feature).
switch on record {
    when Account a {
        // 'a' is typed Account here. No cast, no Schema check.
        a.AccountNumber = generateAccountNumber();
    }
    when Contact c {
        c.LastName = c.LastName?.toUpperCase();
    }
    when null {
        // Reached only when 'record' itself is null — safe, no NullPointerException.
        return;
    }
    when else {
        // Anything else; the runtime type is something we didn't handle.
        // SILENTLY SKIPPING this branch is the #1 latent bug — declare it explicitly.
        System.debug('Unhandled SObjectType: ' + record.getSObjectType());
    }
}
// Form 2 — match by exact value (rare for SObjects; mostly for primitives).
switch on someInteger {
    when 1, 2, 3 { /* multi-value matching only works for primitives + enums */ }
    when else    { ... }
}

Three rules that bite people

  1. No fall-through. Each when branch executes and the switch exits. There is no break keyword and no implicit fall-through to the next branch (unlike C / Java's switch).
  2. when null matches a null expression. It does NOT match a record whose Id is null. It is reached only when the expression itself is null. This makes switch null-safe by design — no NullPointerException from the dispatch.
  3. Type.forName(...) is NOT a substitute for SObjectType. Type.forName('Account') returns System.Type, which is not what switch on accepts in this form. To dispatch from a string name, look up the SObject prototype: Schema.getGlobalDescribe().get(name).newSObject() and switch on that.

Common Patterns

Pattern A — Polymorphic trigger handler

When to use. A single handler that runs across multiple SObject types (common in framework code, templates/apex/TriggerHandler.cls).

public class PolyHandler {
    public static void handle(List<SObject> records) {
        for (SObject record : records) {
            switch on record {
                when Account a   { handleAccount(a); }
                when Contact c   { handleContact(c); }
                when Lead l      { handleLead(l); }
                when else        {
                    // Programmer error — every type the framework registers must have a branch.
                    throw new HandlerException('Unhandled type: ' + record.getSObjectType());
                }
            }
        }
    }
}

Why not the alternative. A chain of if (record instanceof Account) { Account a = (Account) record; ... } is verbose and re-introduces casts that the binding form already handled.

Pattern B — Classifier with explicit unhandled-case

When to use. You handle some types and want all other types to fall through harmlessly with a logged warning.

public static String classify(SObject record) {
    switch on record {
        when Account a   { return a.Type;            }
        when Contact c   { return c.LeadSource;      }
        when null        { return null;              }
        when else        {
            // Crucial: without this branch, the method returns the default null
            // and the caller has no signal that 'record' was an unrecognized type.
            ApplicationLogger.warn('classify: unhandled ' + record.getSObjectType());
            return null;
        }
    }
}

Pattern C — Dispatch from a string type name

When to use. The caller has the type name as a string (config-driven dispatch, custom-metadata-driven router).

public static SObject createByName(String objectApiName) {
    Schema.SObjectType t = Schema.getGlobalDescribe().get(objectApiName);
    if (t == null) {
        throw new IllegalArgumentException('Unknown SObject: ' + objectApiName);
    }
    SObject prototype = t.newSObject();
    switch on prototype {
        when Account a   { return new Account(Name = 'Default Name');     }
        when Contact c   { return new Contact(LastName = 'Default Last'); }
        when else        {
            // The string named a real SObject, but this method only constructs a few.
            throw new IllegalArgumentException('Unsupported for default-create: ' + objectApiName);
        }
    }
}

Type.forName(objectApiName) does NOT work as the switch expression here — it returns System.Type, not an SObject prototype.


Decision Guidance

SituationUse thisReason
Trigger handler dispatching across 3+ SObject typesswitch on record with one branch per typeTyped bindings, explicit when else, no casts
Single-type check ("is this an Account?")record.getSObjectType() == Account.SObjectTypeSwitch overhead not worth it for one check
Dispatch from a string SObject API nameSchema.getGlobalDescribe().get(name).newSObject() then switch on the prototypeType.forName() returns System.Type, not SObjectType
Heterogeneous list with frequent unhandled typesSwitch with when else returning early or loggingSilent-skip is the failure mode without when else
Two types share identical handlingTwo when branches calling the same private methodMulti-type-per-when binding is not supported

  1. Confirm runtime dispatch. If the type is known at compile time, write typed code; do not introduce a switch.
  2. List every SObject type the dispatch can see. From the caller, the trigger registration, or the framework contract.
  3. Choose when else semantics. Either (a) throw — programmer error, the dispatch should be exhaustive — or (b) log + return safely. Never omit when else on a non-exhaustive switch.
  4. Place when null before when else if the expression can be null. The when null branch is the null-safety guarantee for the entire switch.
  5. Verify in test. Write a test class that passes one record of every handled type plus one record of an unhandled type; assert the unhandled-case behavior matches step 3.

Review Checklist


Salesforce-Specific Gotchas

  1. Silent skip without when else. A non-exhaustive switch on an unhandled type does nothing — no exception, no warning. Always declare when else. (See references/gotchas.md § 1.)
  2. Type.forName('Account') does not produce a switch-able expression. It returns System.Type. You need Schema.getGlobalDescribe().get('Account').newSObject() to get an SObject prototype. (See references/gotchas.md § 2.)
  3. Multi-type-per-when is not supported. when Account a, Contact c { ... } does not compile. (See references/gotchas.md § 3.)
  4. when null matches the expression being null, not an SObject with Id = null. Don't use it for "new record" detection. (See references/gotchas.md § 4.)
  5. Managed-package custom SObjects need the namespace. when my_pkg__Custom_Object__c c { ... } — the namespace is part of the type identity. (See references/gotchas.md § 5.)

Output Artifacts

ArtifactDescription
Refactored ApexSwitch-based dispatch replacing instanceof chains, with typed bindings per branch
Test classOne record per handled type + one unhandled type; asserts when else behavior
when else decision rationaleOne sentence per refactor: "throw" or "log + safe-return"

Package files