Hey folks, Abhi here!
Quick question to start: how many times have you written this line without thinking twice?
apex
Schema.SObjectType t = Schema.getGlobalDescribe().get(objectName);
Be honest. It’s muscle memory at this point. It works, your tests pass, you move on with your life. But here’s the thing — that one harmless-looking line is often the most expensive way to do the thing you actually wanted to do. And if your code is heading anywhere near an AppExchange security review, one of these patterns will get you flagged.
So let’s break down the three usual suspects for resolving and describing an SObject dynamically — what each one actually costs, when to reach for it, and when to keep your hand in your pocket. No fluff, just the stuff that’ll make your Apex leaner and your security reviewer happier.
The contenders, at a glance
Before we dig in, here’s the lineup:
Schema.getGlobalDescribe()— hands you aMap<String, SObjectType>of every object in the org. Describes everything. Doesn’t resolve Apex classes.Schema.describeSObjects(List<String>)— describes only the names you pass in. Scoped to exactly what you asked for. Doesn’t resolve Apex classes.Type.forName(name)— returns aSystem.Typeyou can callnewInstance()on. A single type lookup. And yes — it resolves Apex class names too. (Hold that thought.)- The forgotten one:
Account.SObjectType— the static token. Zero lookup cost, resolved at compile time. Most people forget it exists.
Now let’s actually use them.
1. getGlobalDescribe() — the sledgehammer
getGlobalDescribe() builds a map of all objects in your org. Standard, custom, and every single object dragged in by every managed package you’ve ever installed.
apex
// You wanted ONE object. You just described thousands.SObjectType t = Schema.getGlobalDescribe().get('Account');
Here’s the problem: the cost scales with the size of your org, not with your need. In a clean dev org, this feels free — you’ll never notice it. But take that same line into a mature enterprise org running 30 managed packages, and that map can hold thousands of entries. That’s real CPU time to build it and real heap to hold it.
Now, to be fair — describes are cached per transaction, so it’s the first call that’s expensive, not every call. But you’ve still paid for describing thousands of objects you’re never going to touch. That’s the part that stings.
Use it when: you genuinely need to enumerate or iterate every object — a metadata explorer, a generic admin tool, a “list all objects” picker. When “everything” is literally what you need, this is the right tool.
Don’t use it when: you already know the object’s name. Which, let’s be real, is the 95% case.
2. describeSObjects() — the scalpel
apex
SObjectType t = Schema.describeSObjects( new List<String>{ 'Account' })[0].getSObjectType();
This describes only the objects you name. One object, one describe. Need a handful? Pass them all in one call:
apex
List<DescribeSObjectResult> results = Schema.describeSObjects( new List<String>{ 'Account', 'Contact', 'Opportunity' });
The cost here is proportional to what you actually asked for — independent of how bloated your org is. That’s the whole point.
One gotcha: it throws if you pass an invalid name. So when the input comes from a user, config, or a payload, wrap it:
apex
private static SObjectType resolveSObjectType(String objectName) { if (String.isBlank(objectName)) { throw new AuraHandledException('Object name is required.'); } try { return Schema.describeSObjects( new List<String>{ objectName } )[0].getSObjectType(); } catch (Exception e) { throw new AuraHandledException( 'Object "' + objectName + '" not found or not accessible.' ); }}
Bonus tip — lazy loading: there’s an overload, describeSObjects(types, SObjectDescribeOptions.DEFERRED), that defers loading the expensive child-relationship metadata until you actually ask for it. If you only need field info and not relationships, this trims the cost even further. Most people don’t know it’s there.
Use it when: the object name is dynamic — from config, a payload, a UI input — and you only want that object’s metadata. Honestly, this should be your default for dynamic describe.
3. Type.forName() — the wrong tool for SObjects (usually)
You can resolve an SObject type this way:
apex
SObjectType t = ((SObject) Type.forName('Account').newInstance()).getSObjectType();
But look closely at what just happened: you instantiated an object just to ask it what type it is. That’s already weird. But it gets worse, because Type.forName doesn’t only resolve SObjects — it resolves Apex class names too. Feed it user-controlled input and .newInstance() will happily run the no-arg constructor of whatever class matches that string.
That’s two problems stacked on top of each other:
- Performance: you built an instance you never needed.
- Security: instantiating arbitrary types from user-controlled input is exactly the pattern an AppExchange security reviewer flags. And relying on the
(SObject)cast to fail afterward? Too late — the constructor already ran.
So when should you use it? When you’re doing genuine dynamic Apex — a factory pattern, custom-metadata-driven dispatch, plugin architecture. Stuff where instantiating a class by name is the actual goal:
apex
// Legitimate use: build a handler chosen by config — not an SObject lookupMyInterface handler = (MyInterface) Type.forName(config.ApexClassName__c).newInstance();
Don’t use it when: you just want an SObjectType or a describe. Use describeSObjects instead — it physically can’t be tricked into running a constructor.
The one everyone forgets: the static token
Here’s the kicker. If you know the object at compile time, you don’t need to describe anything at all:
apex
SObjectType t = Account.SObjectType; // freeDescribeSObjectResult d = Account.SObjectType.getDescribe();
Zero lookup cost. Whenever the object is hard-coded, reach for this first. No map, no list, no instantiation. It’s the cleanest option and it’s sitting right there.
A quick word on governor limits
You might be thinking, “Abhi, describes don’t even count against my SOQL limit, so who cares?” Fair point — describe calls don’t consume SOQL queries, and modern API versions removed the old hard cap on the number of describes you can run.
But “no hard limit” doesn’t mean “free.” The real ceilings here are CPU time and heap size — and that’s precisely where getGlobalDescribe() quietly eats your transaction alive in a big org. And remember, describes are cached per transaction, so the cost is front-loaded on first access, not spread out per call. Plan accordingly.
TL;DR — which one, when
- Object known at compile time →
Account.SObjectType - Dynamic name, need one (or a few) objects →
Schema.describeSObjects(List<String>) - You truly need to iterate every object →
Schema.getGlobalDescribe() - Instantiating an Apex class by name →
Type.forName(...).newInstance() - Resolving an SObject type from user input → not
Type.forName— usedescribeSObjects
The rule of thumb is simple: describe exactly what you need, no more. Reach for getGlobalDescribe() only when “everything” is what you need, and keep Type.forName for dynamic class loading — never as a backdoor SObject resolver, especially in code headed for security review.
So next time your fingers start typing getGlobalDescribe().get(...) out of habit — pause. Ask yourself: do I actually need the whole org? Most of the time, the answer’s no. And swapping that one line for describeSObjects cuts the per-call cost, sidesteps the Type.forName security smell, and sails a lot cleaner through any AppExchange-readiness checklist. Same line of code, massively better behavior.
Catch you in the next one!
Leave a comment