SfSkills Domains Docs GitHub

Home / apex / apex-schema-describe

apex-schema-describe

Apex Schema describe API patterns — `Schema.getGlobalDescribe()`, `SObjectType.getDescribe()`, `DescribeFieldResult`, `getPicklistValues()`, the per-namespace describe cost, the lazy-vs-eager caching pattern, `SObjectField.getDescribe()` overhead at scale, and the FLS / record-type metadata access patterns. NOT for building dynamic SOQL or dynamic field get/put — use apex/dynamic-apex. NOT for the Tooling API metadata layer — use apex/tooling-api-patterns.

Open the skill folder on GitHub


Apex Schema Describe

Schema describe is one of the most-used and most-misused APIs in Apex. Schema.getGlobalDescribe() looks free — it isn't. Calling SObjectField.getDescribe() per record in a 200-record trigger multiplies a small cost into a CPU-budget problem. FLS checks against DescribeFieldResult.isAccessible() look correct — but the API surface has subtle differences between Schema.SObjectType.newSObject() describe and Schema.DescribeFieldResult describe.

This skill covers the patterns that make Schema describe fast and correct: caching, FLS checks, picklist enumeration, and the loop-cost trap.

NOT for SOQL string-binding (apex/dynamic-soql); NOT for Tooling API metadata work (apex/tooling-api).


Before Starting


Core Concepts

Static-vs-runtime SObjectType lookup

// Compile-time: zero describe cost
Schema.SObjectType t = Account.SObjectType;

// Runtime (string): one global-describe lookup
Schema.SObjectType t = Schema.getGlobalDescribe().get('Account');

Schema.getGlobalDescribe() is the expensive call — it materializes the entire org's SObject inventory. Cache the result if you call it more than once per transaction.

Caching describe results

public class SchemaCache {
    private static final Map<String, Schema.SObjectType> TYPES =
        Schema.getGlobalDescribe();
    private static final Map<String, Map<String, Schema.SObjectField>> FIELDS_BY_TYPE =
        new Map<String, Map<String, Schema.SObjectField>>();

    public static Schema.SObjectField field(String objName, String fieldName) {
        Map<String, Schema.SObjectField> fields = FIELDS_BY_TYPE.get(objName);
        if (fields == null) {
            Schema.SObjectType t = TYPES.get(objName);
            if (t == null) return null;
            fields = t.getDescribe().fields.getMap();
            FIELDS_BY_TYPE.put(objName, fields);
        }
        return fields.get(fieldName);
    }
}

static final ensures the global describe runs once per class load. Per-type field maps are lazy.

FLS check via DescribeFieldResult

Schema.DescribeFieldResult dfr = Account.SSN__c.getDescribe();
if (!dfr.isAccessible()) {
    throw new SecurityException('No FLS read');
}
if (!dfr.isUpdateable()) {
    throw new SecurityException('No FLS update');
}

Modern alternative — Security.stripInaccessible (preferred for DML) handles bulk records:

SObjectAccessDecision dec = Security.stripInaccessible(
    AccessType.UPDATABLE, records
);
update dec.getRecords();

Picklist value enumeration

List<Schema.PicklistEntry> entries =
    Account.Industry.getDescribe().getPicklistValues();

List<String> activeValues = new List<String>();
for (Schema.PicklistEntry pe : entries) {
    if (pe.isActive()) {
        activeValues.add(pe.getValue());  // API name; use getLabel() for display
    }
}

Inactive entries are still in the list — filter explicitly.


Common Patterns

Pattern A — Hot-path describe with class-level cache

public class FieldValidator {
    private static final Map<String, Schema.SObjectField> FIELDS =
        Account.SObjectType.getDescribe().fields.getMap();

    public static Boolean isAccessible(String fieldName) {
        Schema.SObjectField f = FIELDS.get(fieldName);
        return f != null && f.getDescribe().isAccessible();
    }
}

static final + per-type field map = describe runs once per class load.

Pattern B — Bulk FLS via stripInaccessible

public static List<Account> readAccessible(List<Account> records) {
    SObjectAccessDecision dec = Security.stripInaccessible(
        AccessType.READABLE, records
    );
    return (List<Account>) dec.getRecords();
}

Removes inaccessible field values from the records — bulk-safe, governor-cheap.

Pattern C — Dynamic SObject creation by name

public static SObject create(String objName) {
    Schema.SObjectType t = Schema.getGlobalDescribe().get(objName);
    if (t == null) throw new IllegalArgumentException(objName);
    return t.newSObject();
}

Use Schema.getGlobalDescribe().get(name) — NOT Type.forName(name). Type.forName returns System.Type, not an SObject prototype.


Decision Guidance

SituationApproachReason
Compile-time-known SObject typeAccount.SObjectType directlyZero describe cost
Runtime SObject name as stringSchema.getGlobalDescribe().get(name)The only path for string-driven
Repeated describe in a loopCache as static finalAvoid per-iteration describe overhead
FLS check before bulk DMLSecurity.stripInaccessibleBulk-safe; cleaner than per-field
FLS check on a single fieldgetDescribe().isAccessible()Single-field is fine direct
List of active picklist valuesgetPicklistValues() + isActive() filterInactive entries still in the list
RecordType lookup by namegetRecordTypeInfosByDeveloperName().get(name)Stable identifier across orgs
Schema query on managed-package objectInclude namespace prefixRequired for cross-package access

  1. Identify call frequency. Loop body = cache. One-shot = anywhere.
  2. Use compile-time references when possible. Skip describe entirely.
  3. For dynamic strings, cache Schema.getGlobalDescribe() once.
  4. For FLS on bulk DML, use Security.stripInaccessible.
  5. For picklist enumeration, filter isActive() explicitly.
  6. Test the namespaced case if managed packages are in scope.

Review Checklist


Salesforce-Specific Gotchas

  1. Schema.getGlobalDescribe() is expensive. Cache. (See references/gotchas.md § 1.)
  2. getDescribe() on a field in a loop multiplies cost. Cache the field map at class scope. (See references/gotchas.md § 2.)
  3. Inactive picklist entries appear in getPicklistValues(). Filter. (See references/gotchas.md § 3.)
  4. Type.forName returns System.Type, not SObjectType. Use Schema.getGlobalDescribe().get(). (See references/gotchas.md § 4.)
  5. Managed-package fields require the namespace prefix. (See references/gotchas.md § 5.)

Output Artifacts

ArtifactDescription
Cached schema utility classstatic final field maps; one place to evolve
FLS-aware DML helpersstripInaccessible-based wrappers for bulk DML
Picklist enumeration helpersActive-only value lists with label-vs-API distinction

Package files