The Curious Nature of the Salesforce Boolean
- David Reed
The Boolean can be among the simplest data types: it is either true or false, full stop, no complications.
On the Salesforce platform, this could hardly be further than the truth.
Consider the following bug, in a simplified example:
That ought to be fine - we're
asserting a Boolean value, which is what asserts are for. (Although one might reasonably expect the assertion itself to fail here). But in fact we get not an assertion failure or success, but a
NullPointerException. This reveals several things, both specific and broad:
- The value of
nullwhen not initialized;
nullis not equivalent to
System.assert()method throws an exception when passed
null, but not a failed assertion as such;
- Boolean variables in Apex are actually tri-valued: they can be
null, and they default to
Note that it doesn't matter here whether we define
myProperty using property syntax or as a simple Boolean instance variable. The behavior is the same.
It's the case in other contexts as well that referencing a
null Boolean value produces a
NullPointerException rather than evaluating to
false. (This can be particularly confusing to debug, as we're conditioned to look for property access when troubleshooting a
NullPointerException). These conditionals, for example, produce
Boolean b ; System.; // NullPointerException if System.; // NullPointerException
At a certain level, this is fair enough. We have some weird behaviors to look out for, but there's nothing all that wrong with tri-valued Booleans (although it's arguable that the behavior of
System.assert(null) in particular is poorly designed). We should never rely on uninitialized values in our classes or local variables anyway, but always explicitly initialize them.
What's perhaps more confusing, though, is that there are other layers of the Salesforce platform where Booleans are not treated as tri-valued.
Booleans and SOQL
Consider SOQL. The SOQL and SOSL Reference has this to say on filtering on Booleans:
You can use the Boolean values TRUE and FALSE in SOQL queries. To filter on a Boolean field, use the following syntax:
WHERE BooleanField = TRUE
WHERE BooleanField = FALSE
But we learn elsewhere in the reference another facet:
In a WHERE clause that uses a Boolean field, the Boolean field never has a
nullis treated as
false. Boolean fields on outer-joined objects are treated as
falsewhen no records match the query.
So Booleans in the database and in Apex are tri-valued, but in SOQL are treated as binary-valued. A filter in SOQL against
null is treated exactly like one written against
false, so the following queries are treated as equivalent:
[SELECT Id FROM Boolean_Object__c WHERE Boolean__c = false] [SELECT Id FROM Boolean_Object__c WHERE Boolean__c = null]
Given this sort of almost-mismatch between SOQL and Apex, one might suppose that one could see the following happen, if we really drive a wedge into the gap:
Boolean_Object__c b ; System.; // Should pass ; System.; // Should pass insert b; Boolean_Object__c c ; System.; // Should fail.
But in fact we don't, because sObject instances don't behave like other Apex classes. In fact, assertion 1 fails, because Booleans are initialized not to
null but to
false in sObject class instances. Further, we'll get a
insert b, because
null is not treated as a legal value for inserting Boolean (Checkbox) fields. We can't actually create a situation where there's a
null Boolean in the database.
So Booleans are tri-valued in Apex, but are clamped to
false around DML statements and SOQL queries, and there are some special-case behaviors to remain aware of. In particular, users of wrapper or shadow Apex classes from which sObject instances are ultimately generated should keep in mind that the Boolean initialization behaviors differ between the two class types.
Consequences: Hierarchy Custom Settings
One area where this curious Boolean behavior has practical consequences is the Hierarchy Custom Setting. Custom Settings, of course, are custom objects, and they can contain checkbox fields. But Hierarchy Custom Settings have a unique feature allowing them to cascade populated field values down the hierarchy (Organization to Profile to User) until overriden by a non-null value at a lower level.
Suppose we have a custom setting
Instance_Settings__c, with a single Checkbox field
Run__c and some arbitrary set of other fields - say
Test__c, a text field.
If we populate these fields at the Organization level (
UserInfo.getOrganizationId()), we expect rightly that the values at the Organization level will cascade down to the User level, if they're not overriden. And for
Test__c, our text field, that's true. If we set that field to
"foo" at the Organization level, and
null at the User level, sure enough our
Instance_Settings__c.getInstance() value will inherit the Organization's value
But Booleans work differently, because they're treated as binary-valued here - they're never
null. So a
true value for
Run__c will never cascade down to the User or Profile level of our Custom Setting if an instance is populated at that level. The instances at those levels always have
false set for that Checkbox field if we don't explicitly populate
true upon creation at that setup level.
Instance_Setting__c s ; ; ; ; insert s; Instance_Setting__c instance ; System.; // Passes System.; // Passes s ; ; ; insert s; instance ; System.; // Passes System.; // Fails
The Upshot is...
- Booleans are more complicated that one might expect.
- Never rely on behavior around uninitialized variables (in any language!)
NullPointerExceptioncan arise even without a dot-notation object dereference.
- Booleans can't be relied upon to cascade in Hierarchy Custom Settings.