Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Apex

Initial term of field expression must be a concrete SObject: <type>

You wrote `someThing.Field__c` where `someThing` isn't a specific SObject type — usually an `SObject` or `Object` reference. Apex needs the concrete type at compile time to resolve the field name. Cast to the specific SObject before accessing fields.

Also seen asInitial term of field expression must be a concrete SObject·must be a concrete SObject·concrete SObject apex

A developer is refactoring a generic record-processing utility that handles multiple object types. The class compiles. Tests run. Then somewhere in the production logs an exception appears: System.SObjectException: Initial term of field expression must be a concrete SObject: Account. The line that throws is reading a field via dynamic SObject access. The variable is typed correctly. The actual runtime object turns out to be a generic SObject, not a real Account.

What the platform is checking

Apex distinguishes between concrete SObject types (Account, Opportunity, Case, MyCustomObject__c) and the abstract SObject base type. Some operations work on either. Dot-notation field access does not. Reading account.Industry requires the runtime to know that the variable holds a concrete Account; if the variable is typed as SObject at compile time, the runtime needs more help.

The compiler accepts code like this:

SObject record = ...;
String name = (String) record.get('Name');

The get method works on the abstract SObject. But the dot-syntax form does not:

SObject record = ...;
String name = record.Name;  // Compile error or runtime exception

The runtime exception you see (Initial term of field expression must be a concrete SObject) fires when the code is written using dot-syntax on a value the compiler thought might be a generic SObject. Sometimes the compiler catches this at compile time; sometimes the type information is dynamic enough that the check is deferred to runtime.

The "initial term" in the message refers to the leftmost variable in a chain like account.Owner.Name. Apex needs the leftmost term to be a concrete SObject for the dot-chain to compile.

The broken example

A common shape: a service method that takes a generic SObject and tries to read its fields by dot-syntax:

public class RecordSummarizer {
    public static String describe(SObject record) {
        // Compiler hint: SObject is too generic for dot-access
        return record.Name + ' (' + record.Industry + ')';
    }

    public static void run() {
        Account a = [SELECT Name, Industry FROM Account LIMIT 1];
        System.debug(describe(a));
    }
}

When describe runs, the runtime sees that record is parameterized as SObject. Even though the actual object passed in is an Account, the static type information has been lost. The expression record.Name throws the SObjectException.

A second shape: a generic method that takes a list of records and tries to read a relationship:

public static List<String> getOwners(List<SObject> records) {
    List<String> names = new List<String>();
    for (SObject r : records) {
        names.add(r.Owner.Name);  // Throws
    }
    return names;
}

The dot-chain through Owner.Name requires the runtime to know that r is a concrete object with an Owner relationship. The generic SObject type doesn't carry that information.

A third shape: a JSON deserialization that returns a generic SObject:

String body = '{"Name":"Acme","Industry":"Technology"}';
SObject obj = (SObject) JSON.deserialize(body, SObject.class);
String name = obj.Name;  // Throws

JSON deserialization to the abstract SObject type produces a generic object the compiler can't reason about. The dot-syntax fails.

The fix, three paths

Cast to the concrete type. When you know the runtime type, an explicit cast restores the dot-syntax. The cast also gives readers of the code a clear signal about what's expected.

public static String describe(SObject record) {
    Account a = (Account) record;
    return a.Name + ' (' + a.Industry + ')';
}

The cast succeeds at runtime as long as the actual object is an Account. If you pass a Contact, the cast throws ClassCastException, which is clearer than the SObject error and easier to diagnose.

Use get for generic access. When the method genuinely accepts multiple object types, use the SObject get method instead of dot-syntax. The method takes a field name as a string and returns an Object.

public static String describe(SObject record) {
    String name = (String) record.get('Name');
    String industry = (String) record.get('Industry');
    return name + ' (' + industry + ')';
}

The downside is that the field names are now string literals, which means typos compile fine but fail at runtime. The upside is that the code works for any object that has the named fields.

Make the method type-parameterized. If the method's logic is the same for every type, generics let you keep the dot-syntax while accepting different types. Apex's generics support is limited, but a wrapper class with a typed constructor works:

public class TypedSummarizer {
    private Account record;
    public TypedSummarizer(Account a) { this.record = a; }
    public String describe() {
        return record.Name + ' (' + record.Industry + ')';
    }
}

For multi-type support, write parallel classes or use the get method approach.

The fixed example

A summarizer that uses get for type-agnostic access:

public class RecordSummarizer {
    public static String describe(SObject record) {
        Schema.SObjectType objType = record.getSObjectType();
        String typeLabel = objType.getDescribe().getLabel();
        Object nameValue = record.get('Name');
        return typeLabel + ': ' + nameValue;
    }
}

This works on Account, Contact, Opportunity, or any record that has a Name field. The getSObjectType call resolves at runtime, so the code is genuinely polymorphic.

When you do need specific fields per type, branch on the SObject type:

public static String describeWithDetails(SObject record) {
    Schema.SObjectType objType = record.getSObjectType();
    String label = objType.getDescribe().getLabel();
    if (objType == Account.SObjectType) {
        Account a = (Account) record;
        return label + ': ' + a.Name + ' (' + a.Industry + ')';
    } else if (objType == Opportunity.SObjectType) {
        Opportunity o = (Opportunity) record;
        return label + ': ' + o.Name + ' (Stage: ' + o.StageName + ')';
    }
    return label + ': ' + record.get('Name');
}

Each branch casts to a concrete type before reading fields. Dot-syntax works inside each branch.

When generic SObject access actually wins

There are scenarios where the get method is genuinely the right choice, not a workaround. A field-update audit framework that records every change to every object benefits from operating on SObject generically. The same code handles Account, Opportunity, MyCustom__c without case-by-case branches.

public class FieldChangeLogger {
    public static void logChanges(SObject oldRecord, SObject newRecord) {
        Map<String, Schema.SObjectField> fields = newRecord.getSObjectType().getDescribe().fields.getMap();
        for (String fieldName : fields.keySet()) {
            Object oldValue = oldRecord.get(fieldName);
            Object newValue = newRecord.get(fieldName);
            if (oldValue != newValue) {
                System.debug(fieldName + ': ' + oldValue + ' -> ' + newValue);
            }
        }
    }
}

The logger doesn't know or care about the concrete type. It iterates the describe metadata. Generic access is the natural design here.

Trigger handlers with multiple object types

A team that wants a shared trigger framework often runs into the SObject-vs-concrete question. The framework receives a list of records as List<SObject> because Trigger.new comes through as a typed list of the trigger's object. A naive framework might do:

public class TriggerFramework {
    public static void afterInsert(List<SObject> records) {
        for (SObject r : records) {
            if (r.Name == null) {  // Fails for the generic type
                // ...
            }
        }
    }
}

The fix is to make the framework operate on SObject via get calls, or to dispatch to type-specific handlers based on the trigger's SObjectType:

public class TriggerFramework {
    public static void afterInsert(List<SObject> records, Schema.SObjectType objType) {
        if (objType == Account.SObjectType) {
            AccountTriggerHandler.afterInsert((List<Account>) records);
        } else if (objType == Case.SObjectType) {
            CaseTriggerHandler.afterInsert((List<Case>) records);
        }
    }
}

Each handler operates on a concrete list and can use dot-syntax. The framework's job is dispatch, not field access.

Edge case: dynamic SOQL

Code that builds SOQL strings at runtime and runs them via Database.query gets back a List<SObject>. The return type is generic by design.

String soql = 'SELECT Id, Name FROM ' + objectName;
List<SObject> results = Database.query(soql);
for (SObject r : results) {
    System.debug(r.get('Name'));  // Use get, not dot-syntax
}

If the caller knows the concrete type, cast the whole list:

List<Account> accounts = (List<Account>) Database.query('SELECT Id, Name FROM Account');
for (Account a : accounts) {
    System.debug(a.Name);  // Dot-syntax works after cast
}

The cast is checked at runtime. If the SOQL returns a different type, the cast throws.

Edge case: relationship traversal

Reading a parent field through a child reference requires the parent reference to be a concrete SObject too. r.Owner.Name is a chain: r must be concrete (or cast to concrete), and Owner must be a concrete User reference (which it is, since Owner is typed as User in the schema).

For deeper traversal:

Case c = [SELECT Id, Account.Industry FROM Case LIMIT 1];
String industry = c.Account.Industry;

The Account.Industry chain works because each step is a concrete schema reference. The SOQL projects the relationship, and the runtime resolves the field through the parent record.

A subtle case: lists from Trigger.new in shared helpers

A helper that accepts List<SObject> from triggers across multiple objects looks tidy but invites the SObject error the first time someone uses dot-syntax inside the helper:

public class AuditHelper {
    public static void writeAudit(List<SObject> records, String objectName) {
        for (SObject r : records) {
            // dot-syntax fails here even though the runtime knows what r is
            insert new Audit_Entry__c(Object_Name__c = objectName, Record_Name__c = r.Name);
        }
    }
}

The fix is to either accept the concrete list type per object (a small refactor that opens room for object-specific behavior anyway) or to use r.get('Name') so the generic SObject works uniformly. Each call site picks a side; the helper stays consistent within itself.

Apex generics, the limited version

Apex doesn't support full Java-style generics, but it does support typed collections. Map<Id, SObject> is a parameterized collection where the value is generic; Map<Id, Account> is a parameterized collection where the value is concrete. Most helpers should accept the concrete variant whenever the caller knows the type, because it preserves dot-syntax everywhere downstream.

When you genuinely need a single helper that operates on multiple types, the design choices are: branch internally by type and cast, accept the generic SObject and use get, or implement an interface that each concrete type provides. The interface approach is the cleanest in larger codebases because it makes the contract explicit. The downside is the boilerplate of an interface declaration on every concerned type.

Defensive habits

Avoid generic SObject as a method parameter when the method's logic is type-specific. Type-specific methods belong in type-specific classes. The naming clarifies intent.

When generic methods are genuinely useful, document the expected types and the access pattern. Inline comments help future readers understand why get is used instead of dot-syntax. A short paragraph at the top of the class describing the contract pays off six months later.

Cast early, not late. A method that begins with Account a = (Account) record; is easier to read than one that scatters (Account) casts throughout. The single cast also moves the failure mode (ClassCastException for a wrong type) to a single observable line in the stack trace.

Treat the Database.query return type as a signal to be careful. The result is a generic list. Decide immediately whether to cast (you know the type) or iterate as SObject (you don't). Mixing the two patterns in the same method tends to produce code that compiles but throws on edge cases.

Add a small unit test for every helper that uses SObject.get. The test passes objects of each expected type and verifies the helper returns sensible values. The test catches typos in field-name strings, which the compiler never catches.

Pair generic-SObject code with explicit field-name constants:

public static final String FIELD_NAME = 'Name';
public static final String FIELD_INDUSTRY = 'Industry';

String label = (String) record.get(FIELD_NAME);

The constants make it obvious which fields the helper expects and catch typos at the constant declaration rather than at runtime.

Quick recovery checklist

When the SObjectException fires:

  1. Read the stack trace; identify the line with the dot-syntax access.
  2. Inspect the variable's declared type. Is it SObject or a concrete type?
  3. If declared as SObject, change to the concrete type if possible.
  4. If the method must accept generic SObject, switch dot-syntax to get calls or add a cast before the dot-syntax.
  5. Re-run the test or reproduce the trigger to confirm.

Most incidents resolve in minutes once the type confusion is found. The longer ones involve frameworks where the design itself accepts overly generic types, and the fix is a small refactor toward type-specific dispatch.

A worked example: refactoring a generic batch class

A batch class that processes any object via the generic SObject type ends up calling get for every field. Refactoring to a type-specific class (one batch class per object type) removes the field-name strings, restores compile-time field validation, and clarifies the intent. The cost is duplication: one class per type instead of one shared class. The benefit is fewer runtime surprises and easier code review. In most codebases the trade-off favors duplication; only the largest projects with truly generic data-processing logic benefit from the generic approach.

When duplication feels excessive, the right move is usually to extract the truly shared logic into a small helper that operates on primitives (Decimal, String, Date) rather than SObjects, and to keep the SObject handling in type-specific code that calls into the helper. The helper has no SObject dependencies; each caller passes already-extracted field values. The shared logic stays DRY without forcing every caller through the generic SObject contract.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Apex errors