7 min read

Typing Polymorphic Fields in Apex

Salesforce makes use of polymorphic relationship fields for native lookup fields that can reference multiple objects. The most common fields that use polymorphic relationships are Owner fields and Activity WhatId/WhoId fields. There may come a time when you need to work with these fields and be able to make certain decisions based on the type of sObject the field represents. For instance, the need to execute some business logic based on a task that is associated to an opportunity.

There are a couple ways you can identify the type of sObject a polymorphic field represents, but the first method I want to show is one that does not work; yet I see it implemented from time to time and I wonder if it was something that had worked at one time or never worked and no one noticed.

The instanceof Operator Does Not Work

The Salesforce documentation on Working with Polymorphic Relationships provides an example where the instanceof operator is used to determine the type of sObject that is represented by a polymorphic field.

Here is an example of how this operator is used

sObject x = new Account();

if (x instanceof Account) {
	System.debug('This is an Account');
}
else {
	System.debug('This is not an Account');
}

...and here is an example using this technique with a Task object to determine if the associated WhatId is referencing an Account or an Opportunity.

Task t = new Task();

if (t.What instanceof Opportunity) {
	System.debug('This is an Opportunity');
}
else {
	System.debug('This is not an Opportunity');
}

Both examples above are syntactically correct and do not produce run-time errors. However, this technique does not work for the latter example using the Task.What field. To validate that it does not work I created a fairly simple unit test that tested this technique in a couple of scenarios.

@isTest static void testPolymorphicTypeUsingInstanceOf() {
	Account account = new Account(name='Test Account');
	insert account;

	Opportunity opp = new Opportunity();
	opp.AccountId = account.Id;
	opp.Name = 'Test Opportunity';
	opp.CloseDate = System.today() + 5;
	opp.StageName = 'Qualified';
	insert opp;

	test.StartTest();

	Task t = new Task();
	t.WhatId = opp.Id;
	t.Subject = 'Test Task';
	t.ActivityDate = System.today();
	
	System.debug('-Before Task Insert-');
	System.debug('Is Account: ' + (t.What instanceof Account));
	System.debug('Is Opportunity: ' + (t.What instanceof Opportunity));
	insert t;
	
	System.debug('-After Task Insert-');
	System.debug('Is Account: ' + (t.What instanceof Account));
	System.debug('Is Opportunity: ' + (t.What instanceof Opportunity));
	
	System.debug('-After Query using What.Id-');
	Task t1 = [SELECT Id, WhatId, What.Id FROM Task WHERE Id=:t.Id];
	System.debug('Is Account: ' + (t1.What instanceof Account));
	System.debug('Is Opportunity: ' + (t1.What instanceof Opportunity));

	System.debug('-After Task Query using What.Name-');
	Task t2 = [SELECT Id, WhatId, What.Name FROM Task WHERE Id=:t.Id];
	System.debug('Is Account: ' + (t2.What instanceof Account));
	System.debug('Is Opportunity: ' + (t2.What instanceof Opportunity));

	Test.stopTest();
}

Here is the log file that shows the results of the test.

29.0 APEX_CODE,DEBUG
23:40:49.229 (1229567000)|EXECUTION_STARTED
23:40:49.229 (1229603000)|CODE_UNIT_STARTED|[EXTERNAL]|01p30000001iB3g|PolymorphicRelationships_Test.testPolymorphicTypeUsingInstanceOf
23:40:49.229 (1229997000)|METHOD_ENTRY|[2]|01p30000001iB3g|PolymorphicRelationships_Test.PolymorphicRelationships_Test()
23:40:49.230 (1230013000)|METHOD_EXIT|[2]|PolymorphicRelationships_Test
23:40:51.105 (3105171000)|USER_DEBUG|[40]|DEBUG|-Before Task Insert-
23:40:51.105 (3105287000)|USER_DEBUG|[41]|DEBUG|Is Account: true
23:40:51.105 (3105340000)|USER_DEBUG|[42]|DEBUG|Is Opportunity: true
23:40:51.126 (3126441000)|CODE_UNIT_STARTED|[EXTERNAL]|01q30000000UmT7|PolymorphicRelationshipTrigger on Task trigger event BeforeInsert for [new]
23:40:51.126 (3126671000)|USER_DEBUG|[3]|DEBUG|BEFORE TRIGGER EVENT
23:40:51.130 (3130576000)|METHOD_ENTRY|[1]|01p30000001iBaZ|PolymorphicRelationship.PolymorphicRelationship()
23:40:51.131 (3131667000)|METHOD_EXIT|[1]|PolymorphicRelationship
23:40:51.131 (3131728000)|METHOD_ENTRY|[4]|01p30000001iBaZ|PolymorphicRelationship.testTaskTypes(LIST<Task>)
23:40:51.132 (3132308000)|USER_DEBUG|[6]|DEBUG|null
23:40:51.132 (3132370000)|USER_DEBUG|[7]|DEBUG|Is Account: true
23:40:51.132 (3132422000)|USER_DEBUG|[8]|DEBUG|Is Opportunity: true
23:40:51.132 (3132463000)|METHOD_EXIT|[4]|01p30000001iBaZ|PolymorphicRelationship.testTaskTypes(LIST<Task>)
23:40:51.132 (3132548000)|CODE_UNIT_FINISHED|PolymorphicRelationshipTrigger on Task trigger event BeforeInsert for [new]
23:40:51.410 (3410130000)|CODE_UNIT_STARTED|[EXTERNAL]|01q30000000UmT7|PolymorphicRelationshipTrigger on Task trigger event AfterInsert for [00T3000002YFZAr]
23:40:51.410 (3410486000)|USER_DEBUG|[8]|DEBUG|AFTER TRIGGER EVENT
23:40:51.410 (3410540000)|METHOD_ENTRY|[9]|01p30000001iBaZ|PolymorphicRelationship.testTaskTypes(LIST<Task>)
23:40:51.411 (3411651000)|USER_DEBUG|[6]|DEBUG|00T3000002YFZArEAP
23:40:51.411 (3411704000)|USER_DEBUG|[7]|DEBUG|Is Account: true
23:40:51.411 (3411736000)|USER_DEBUG|[8]|DEBUG|Is Opportunity: true
23:40:51.411 (3411778000)|METHOD_EXIT|[9]|01p30000001iBaZ|PolymorphicRelationship.testTaskTypes(LIST<Task>)
23:40:51.411 (3411858000)|CODE_UNIT_FINISHED|PolymorphicRelationshipTrigger on Task trigger event AfterInsert for [00T3000002YFZAr]
23:40:51.414 (3414725000)|USER_DEBUG|[45]|DEBUG|-After Task Insert-
23:40:51.414 (3414792000)|USER_DEBUG|[46]|DEBUG|Is Account: true
23:40:51.414 (3414839000)|USER_DEBUG|[47]|DEBUG|Is Opportunity: true
23:40:51.414 (3414870000)|USER_DEBUG|[49]|DEBUG|-After Query using What.Id-
23:40:51.457 (3457792000)|USER_DEBUG|[51]|DEBUG|Is Account: false
23:40:51.457 (3457885000)|USER_DEBUG|[52]|DEBUG|Is Opportunity: false
23:40:51.457 (3457922000)|USER_DEBUG|[54]|DEBUG|-After Task Query using What.Name-
23:40:51.469 (3469566000)|USER_DEBUG|[56]|DEBUG|Is Account: false
23:40:51.469 (3469634000)|USER_DEBUG|[57]|DEBUG|Is Opportunity: false
23:40:51.492 (3492763000)|CODE_UNIT_FINISHED|PolymorphicRelationships_Test.testPolymorphicTypeUsingInstanceOf
23:40:51.492 (3492777000)|EXECUTION_FINISHED

As you can see from the log, even when the task is provided by the system in a trigger event the instanceof operator fails to return the correct value. This behavior seems to indicate a bug in the system. If you notice, when I create an instance of a Task from scratch the instanceof operator indicates that the What field is both an Account and Opportunity while all other comparisons performed after querying a Task indicate that it is neither.

A Possible Explanation of Why The instanceof Operator Fails

After further research I found that the reason this might not work is because when querying the type of sObject a WhatId represents it returns 'Name' rather than the expected Account, Opportunity, Case, etc.

Further research shows that a 'Name' object does indeed exist and it exists to support polymorphic relationships. This is why I am able to query the field 'What.Type' in the sample provided below; because 'Type' is actually a reference to a field on the 'Name' object not the actual sObject that the What field represents.

Here is a quote of its purpose from the Name SObject Documentation

This object is used to retrieve information from related records where the related record may be from more than one object type (a polymorphic foreign key). For example, the owner of a case can be either a user or a group (queue). This object allows retrieval of the owner name, whether the owner is a user or a group (queue). You can use a describe call to access the information about parents for an object, or you can use the who, what, or owner fields (depending on the object) in SOQL queries. This object cannot be directly accessed.

Here is the code I used to discover this issue.

Task t = [SELECT Id, WhatId, What.Type FROM Task WHERE WhatId != null LIMIT 1];

System.debug('What.Type: ' + t.What.Type);
System.debug('getSObjectType: ' + t.What.getSObjectType());

if (t.What instanceof Case) {
	System.debug('This is a Case');
}
else {
	System.debug('This is not a Case');
}

Here is the log file that shows the results.

00:59:02.037 (37885000)|EXECUTION_STARTED
00:59:02.037 (37900000)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
00:59:02.038 (38779000)|SOQL_EXECUTE_BEGIN|[1]|Aggregations:0|select Id, WhatId, What.Type from Task where WhatId != null limit 1
00:59:02.061 (61819000)|SOQL_EXECUTE_END|[1]|Rows:1
00:59:02.062 (62211000)|USER_DEBUG|[3]|DEBUG|What.Type: Case
00:59:02.062 (62377000)|USER_DEBUG|[4]|DEBUG|getSObjectType: Name
00:59:02.062 (62494000)|USER_DEBUG|[10]|DEBUG|This is not a Case
00:59:02.595 (62554000)|CUMULATIVE_LIMIT_USAGE
00:59:02.062 (62593000)|CODE_UNIT_FINISHED|execute_anonymous_apex
00:59:02.062 (62600000)|EXECUTION_FINISHED

Whether this is a bug or not, it is clear that (at least as of version 29.0 of the Apex language) we cannot rely on the instanceof operator to determine the object type of a polymorphic field.

A Way That Works

There are two other ways (that I am aware of) to determine the type of sObject that is referenced by a polymorphic field. Each technique has their own pros and cons.

The first technique involves performing a describe call for the sObject that you want to work with and comparing the sObjects id prefix with the first 3 chars of the id that is stored in the WhatId field (or your polymorphic field of choice).

string accountIdPrefix = Schema.SObjectType.Account.getKeyPrefix();

Task t = [SELECT Id, WhatId FROM Task LIMIT 1];
string whatId = t.WhatId;

if (whatId.startsWith(accountIdPrefix) == true) {
	System.debug('This is an Account');
}
else {
	System.debug('This is not an Account');
}

One thing to note about this technique is that in order to compare the key prefix you need to cast the record id to a string which is easily done by passing the id to a string variable. This will allow you to use the startsWith() method to compare the key prefix.

The pros of this technique is that if you already have an instance of an object you can test the type using a describe call. The con is there is an governor limit on the number of describe calls you can make so if you need to use this technique you will need to make sure that you are not using this technique too often or that you have a reusable way of accessing an objects key prefix that will stay within the limits.

The second method uses the 'Type' field that I referenced early in this post. This does require that you have control over the query so if you are doing this as part of a trigger you will need to re-query your dataset to have access to this field.

Here is an example using the 'Type' field.

Task t = [SELECT Id, WhatId, What.Type FROM Task WHERE WhatId != null LIMIT 1];

if (t.What.Type == 'Case') {
	System.debug('This is a Case');
}
else {
	System.debug('This is not a Case');
}

As you can see from the example, once you are able to query for the type of object, testing which object is referenced becomes trivial. This is my preferred method where possible and even though we have been focusing on the Task.What field throughout this post this technique does work for other polymorphic fields such as the Owner field.

Future Techniques for Working With Polymorphic Fields

Now that we have seen how to type polymorphic fields, let's look into the future a bit. Salesforce is currently piloting a new expression for SOQL queries called TYPEOF. This new expression is only available as a developer preview at the moment and according to the documentation is available in API version 26.0 and later.

What is particularly exciting about this new expression is that it provides much more flexibility when working with objects that support polymorphic fields. It should allow for less lines of code because the expression will allow developers to return fields based on the type of sObject that is referenced.

Here is an example from the documentation that illustrates how this expression is used.

SELECT 
  TYPEOF What
	WHEN Account THEN Phone, NumberOfEmployees
	WHEN Opportunity THEN Amount, CloseDate
	ELSE Name, Email
  END
FROM Event

It is not too hard to see based on the example that using this new expression we can perform a sub-query to gather additional information about a related object. Rather than collecting record ids and performing an additional query. Now, I don't have access to this feature just yet but I am willing to bet that given the example above Salesforce will count this as 3 queries just as it does for sub-queries so it won't save us from a query limit.

Also, I should note that this feature comes with a lot of limitations. It's worth jumping over to the SOQL TYPEOF Expression Documentation to read more about these limitations if you are interested in learning more about this feature.