Harnessing the Power of Field Dependency in Apex!
Salesforce supports the concept of a “dependent” picklist, A Dependent picklist is a custom or multi-select picklist for which the valid values depend on the value of another field, called the controlling field. Controlling fields can be any picklist (with at least one and fewer than 300 values) or checkbox field on the same record.
Here’s a brief summary of Controlling & Dependent Picklists:
Controlling Picklist:
A controlling picklist is the primary or parent picklist in a dependent picklist relationship. It determines options in the dependent picklist based on the selection. For example, “Country” selects “United States,” the “State” picklist displays U.S. states.
Dependent Picklist:
A dependent picklist is a secondary or child picklist that dynamically updates its available options based on the selection made in the controlling picklist. It uses the “validFor” field, a binary-encoded field, to ensure users see only relevant options that are valid for a particular choice in the controlling picklist, reducing errors and enhancing data consistency.
Controlling & Dependent Picklists have always been a Configurable Feature in Salesforce. Salesforce Administrators & Developers have been using it since a long period of time in Layouts, Aura, VF & LWCs to streamline data entry and enhance Data Accuracy.
At times, there are Business cases where it becomes essential not to use the Controlling & Dependent Picklists as direct input fields on the VF/Aura/LWC custom components & rather go with an custom approach to display the Picklist values.
One such scenario is when, the Dependent Picklist value is an Inactive Picklist value at the Field Level and you still have to show the same on the VF/Aura/LWC component since that Inactive Dependent Picklist value exists on an Existing Record.
Many Developers have been upvoting the Salesforce Idea based on this :
“Add Describe Field Result Method To Get Dependent Picklist Values”, to add a new method in the Schema object for retrieving all the Controlling Values -vs- their Dependent Picklist values automatically. Until this Idea is delivered as a new Salesforce feature, we’ll have to go with a custom approach to achieve the same.
The whole logic to find the Controlling Picklist values for Each Dependent Picklist value, depends on the “validFor” field.
The “validFor” field is a binary-encoded field present in the Schema.PicklistEntry object & uses bitwise comparison to determine which dependent picklist options are valid for a specific controlling picklist choice.
Each character in the validFor field represents a binary digit (bit) position. In Salesforce, each controlling picklist value corresponds to a specific bit position in the validFor field. For example, if you have a controlling picklist with values A, B, and C, they might be mapped to bit positions as follows:
- A: 1 (binary: 0001)
- B: 2 (binary: 0010)
- C: 4 (binary: 0100)
To determine the Valid Combinations b/w dependent picklist & controlling picklist choice, Salesforce performs a bitwise comparison between the binary representation of the selected controlling picklist value and the validFor field for each dependent picklist option.
For Example: Let’s say you have a controlling picklist with values A, B, and C. The validFor field for a dependent picklist might be as follows:
- Option X: 0011 (valid for A and B)
- Option Y: 0101 (valid for A and C)
- Option Z: 0100 (valid for C)
If a user selects A in the controlling picklist, Salesforce checks the validFor field for each dependent picklist option:
- Option X has the second and third bits set to 1, so it’s valid (A and B).
- Option Y has the first and third bits set to 1, so it’s valid (A and C).
- Option Z doesn’t have the first, third & fourth bits set, so it’s only valid for C.
Salesforce calculates and maintains the “validFor” values in the background, when you create or modify picklist options and their dependencies.
If you’re experiencing issues with your dependent picklists “validFor” values, it’s a good idea to review and potentially recreate the relationships through the standard setup process to ensure proper functionality.
Let’s come to the actual Code!
I have created a Wrapper class which contains 3 Maps which will be storing the following data :
- fieldDependencyMap => Controlling Picklist Values -vs- their Dependent Picklist values.
- controllingFieldAPIValueVSLabel => Controlling Picklist Values API Name vs their Labels.
- dependentFieldAPIValueVSLabel=> Dependent Picklist Values API Name vs their Labels.
Note:
In Apex, we use API Names of the Picklist values, but in Front-end, we have to display the Labels for these Picklist values, hence I’m storing data in ‘controllingFieldAPIValueVSLabel’ & ‘dependentFieldAPIValueVSLabel’ Maps, so that the User Experience remains consistent with the Standard functionality.
/* FieldDependencyWrapper - Wrapper Class
* Author : Shivam Vishwakarma
*/
public class FieldDependencyWrapper{
public Map<String, List<String>> fieldDependencyMap; //FieldDependencyMatrix Map with Key : ControllingField Picklist Value Label ; Value : List of all DependentField Picklist Value Labels
public Map<String, String> controllingFieldAPIValueVSLabel; //ControllingField Map with Key : Picklist Value API Name ; Value = Picklist Value Label
public Map<String, String> dependentFieldAPIValueVSLabel; //DependentField Map with Key : Picklist Value API Name ; Value = Picklist Value Label
FieldDependencyWrapper(Map<String, List<String>> fieldDependencyDataMap, Map<String, String> controllingFieldMap, Map<String, String> dependentFieldMap){
fieldDependencyMap = fieldDependencyDataMap;
controllingFieldAPIValueVSLabel = controllingFieldMap;
dependentFieldAPIValueVSLabel = dependentFieldMap;
}
}
Now there are 2 ways to Compute the Field Dependency Map.
- Approach 1: Base64 Decoding of validFor field & Bitwise Operators
- Approach 2: Base64 Decoding of validFor field, then Converting it to Hex & using Hex values to Compute the Field Dependency Map.
In both the approaches, I have used “FieldDependencyWrapper” to maintain Consistency with the Resultant Output of both the approaches.
Approach 1: Base64 Decoding of validFor field & Bitwise Operators
In order to explain this approach, I would like to first explain what is Base64.
Base64 is a group of binary-to-text encoding schemes that represent binary data (more specifically, a sequence of 8-bit bytes) in sequences of 24 bits that can be represented by four 6-bit Base64 digits.
e.g. Base 64 — “AgAC” = Binary — “000000 100000 000000 000010” = Hex — “020002”
A brief summary of how Salesforce calculates the “validFor” field’s Base64 value in backend.
Let’s suppose we have the following Field Dependency configured.
The “validFor” field would then contain values something as follows:
In background, this “validFor” field is calculated as follows in Base64 format for the first two characters in the validFor field for “Z”.
Note :
Salesforce adds trailing 0’s to the “validFor” field in order to complete the Binary values for each Base64 character.
Code for Approach 1:
/* Version 1
* Author : Shivam Vishwakarma
* Method : getDependentPicklistValuesViaDependentFieldAPI(Schema.sObjectField)
* Params : Field's API Name Reference.
* Approach 1 : Product2.Sub_Category__c
* Approach 2 : Schema.getGlobalDescribe().get( Product2 ).getDescribe().fields.getMap().get( Sub_Category__c )
* Return : Returns a Wrapper object of FieldDependencyWrapper.
* Additional Details:
* A. Method should work in both cases, if the Controlling Field is checkbox or a picklist value.
* B. Map<String, List<String>>
* 1. Map Keyset contains only Picklist values that are Active in the Controlling Field.
* 2. Map Value List contains only Picklist values that are Active in the Dependent Field.
* 3. Map has only API Name related information from both Controlling Field & Dependent Field (As In Apex, we refer to the Picklist value API Names in Code.)
* C. Map<String, String>
* 1. One can refer these Maps for the Controlling & Dependent Fields to fetch the Labels for the Picklist values API Names.
* D. Logical computations in method are based on directly working with Base64 to understand the association b/w Controlling & Dependent picklist values.
*/
public static FieldDependencyWrapper getDependentPicklistValuesViaDependentFieldAPI(Schema.sObjectField dependentFieldToken) {
Map<String,List<String>> fielddependentPicklistValues = new Map<String,List<String>>(); // Key : ControllingField Picklist Value Label ; Value : List of all DependentField Picklist Value Labels
Map<String, String> controllingFieldAPIValueVSLabel = new Map<String,String>(); //Key : Picklist Value API Name ; Value = Picklist Value Label
Map<String, String> dependentFieldAPIValueVSLabel = new Map<String,String>(); //Key : Picklist Value API Name ; Value = Picklist Value Label
String base64Map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
Schema.DescribeFieldResult dependentField = dependentFieldToken.getDescribe(); //Fetch Dependent Field DescribeFieldResult from Schema
Schema.sObjectField controllingFieldToken = dependentField.getController();
if (controllingFieldToken == null){ //If null then the DependentField field provided as input has no Controlling Field.
return new FieldDependencyWrapper(new Map<String,List<String>>(), new Map<String,String>(), new Map<String,String>());
}
Schema.DescribeFieldResult controllingField = controllingFieldToken.getDescribe(); //Fetch Controlling Field DescribeFieldResult from Schema
List<Schema.PicklistEntry> controllingFieldPicklistValueEntries;
Boolean isControllingFieldPicklistType = false;
if(controllingField.getType() != Schema.DisplayType.Boolean){ //Controlling Field can be either a Checkbox(Boolean) or a Picklist
isControllingFieldPicklistType = true;
controllingFieldPicklistValueEntries = controllingField.getPicklistValues();
}
for(Schema.PicklistEntry entry : dependentField.getPicklistValues()){
if( entry.isActive() && String.isNotEmpty(String.valueOf( ((Map<String,Object>) JSON.deserializeUntyped(JSON.serialize(entry))).get('validFor') )) ){
System.debug('DependencyMatrix for entry :'+entry+' : ' +String.valueOf(((Map<String,Object>) JSON.deserializeUntyped(JSON.serialize(entry))).get('validFor')));
List<String> base64charsOfDependencyMatrix = String.valueOf(((Map<String,Object>) JSON.deserializeUntyped(JSON.serialize(entry))).get('validFor')).split('');
Integer controllingValuesSize = (isControllingFieldPicklistType==true && controllingFieldPicklistValueEntries != null) ? controllingFieldPicklistValueEntries.size() : 2;
for(Integer index = 0; index < controllingValuesSize; index++){
Object controlValue = (controllingFieldPicklistValueEntries == null ? (Object) (index == 1)
: (Object) (controllingFieldPicklistValueEntries[index].isActive() ? controllingFieldPicklistValueEntries[index].getValue() : null)
);
Integer bitIndex = index / 6; //Base 64 bit index
if(bitIndex > base64charsOfDependencyMatrix.size() - 1){
break;
}
Integer bitShift = 5 - Math.mod(index, 6); //Move forward by 6 bits
if (controlValue == null || (base64Map.indexOf( base64charsOfDependencyMatrix[bitIndex] ) & (1 << bitShift)) == 0)
continue; //Do not generate the Map entry, if the Controlling field's Picklist value is an Inactive Picklist value
if (!fielddependentPicklistValues.containsKey((String)controlValue)) {
fielddependentPicklistValues.put((String) controlValue, new List<String>());
}
fielddependentPicklistValues.get((String)controlValue).add(entry.getValue());
}
}
}
if(isControllingFieldPicklistType){
for(Schema.PicklistEntry entry : controllingField.getPicklistValues()){
if(entry.isActive())
controllingFieldAPIValueVSLabel.put(entry.getValue(), entry.getLabel());
}
}else{
controllingFieldAPIValueVSLabel.put('true','true'); //Added only true, as code above contains following check : controllingFieldPicklistValueEntries[index].isActive()
}
for(Schema.PicklistEntry entry : dependentField.getPicklistValues()){
if(entry.isActive())
dependentFieldAPIValueVSLabel.put(entry.getValue(), entry.getLabel());
}
return new FieldDependencyWrapper(fielddependentPicklistValues, controllingFieldAPIValueVSLabel, dependentFieldAPIValueVSLabel);
}
Approach 2: Base64 Decoding of validFor field, then Converting it to Hex & using Hex values to Compute the Field Dependency Map.
Generally, programmers are more proficient with Hex, Decimal & Binary than they are with Base64, hence in Approach 2, we are using the Hex values to compute the field dependency map instead of directly using base64.
Adding a Decimal/Binary/Hex/Octal chart, that shows which hex value corresponds to what charset in ASCII/UTF-8.
Code for Approach 2:
/* Version 2
* Author : Shivam Vishwakarma
* Method : getFieldDependencies
* Params : Object's API Name , Controlling & Dependent Field's API Name String References.
* E.g. getFieldDependencies('Product2', 'Category__c', 'Sub_Category__c'));
* Return : Returns a Wrapper object of FieldDependencyWrapper.
* Additional Details:
* A. Method will only work if the Controlling Field is a picklist value.
* B. Map<String, List<String>>
* 1. Map Keyset contains only Picklist values that are Active in the Controlling Field.
* 2. Map Value List contains only Picklist values that are Active in the Dependent Field.
* 3. Map has only API Name related information from both Controlling Field & Dependent Field (As In Apex, we refer to the Picklist value API Names in Code.)
* C. Map<String, String>
* 4. One can refer these Maps for the Controlling & Dependent Fields to fetch the Labels for the Picklist values API Names.
* D. Logical computations in method are based on Decoding Base64 to Hex & then using Hex, Decimal & Binary knowledge to understand the association b/w Controlling & Dependent picklist values.
*/
public class PickListInfoValidForDeserializationClass
{
public String validFor;
}
public static FieldDependencyWrapper getFieldDependencies(String objectName, String controllingField, String dependentField)
{
Map<String,List<String>> fieldDependenciesData = new Map<String,List<String>>(); // Key : ControllingField Picklist Value Label ; Value : List of all DependentField Picklist Value Labels
Map<String, String> controllingFieldAPIValueVSLabel = new Map<String,String>(); //Key : Picklist Value API Name ; Value = Picklist Value Label
Map<String, String> dependentFieldAPIValueVSLabel = new Map<String,String>(); //Key : Picklist Value API Name ; Value = Picklist Value Label
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.DescribeSObjectResult describeResult = objType.getDescribe();
Schema.DescribeFieldResult controllingFieldInfo = describeResult.fields.getMap().get(controllingField).getDescribe();
Schema.DescribeFieldResult dependentFieldInfo = describeResult.fields.getMap().get(dependentField).getDescribe();
List<Schema.PicklistEntry> controllingValues = controllingFieldInfo.getPicklistValues();
List<Schema.PicklistEntry> dependentValues = dependentFieldInfo.getPicklistValues();
for(Schema.PicklistEntry currControllingValue : controllingValues){
controllingFieldAPIValueVSLabel.put(currControllingValue.getValue(), currControllingValue.getLabel());
fieldDependenciesData.put(currControllingValue.getValue(), new List<String>());
}
for(Schema.PicklistEntry currDependentValue : dependentValues)
{
dependentFieldAPIValueVSLabel.put(currDependentValue.getValue(), currDependentValue.getLabel());
PickListInfoValidForDeserializationClass picklistInfo = (PickListInfoValidForDeserializationClass) JSON.deserialize(JSON.serialize(currDependentValue), PickListInfoValidForDeserializationClass.class);
String hexString = EncodingUtil.convertToHex(EncodingUtil.base64Decode(picklistInfo.validFor)).toUpperCase(); //decoding validFor from Base64 to Hex value, so that we can formulate the conditions in Hex, Decimal & Binary, which we all are familiar with.
System.debug('DependentField: Label:' + currDependentValue.getValue() + ' ValidForInHex:' + hexString + ' JsonString:' + JSON.serialize(currDependentValue));
// validFor:AgACgAAA ; ValidForBase64Decoded: 000000 100000 000000 000010 100000 000000 000000 000000 ; ValidForInHex:020002800000 || JsonString:{"active":true,"defaultValue":false,"label":"Antivirus and Security Software","validFor":"AgACgAAA","value":"Antivirus and Security Software"}
Integer baseCount = 0; //Decimal [0-15] = Hex [0-F] = Binary [0000 - 1111], hence baseCount has to start from 0 & has to be incremented by 4 places.
for(Integer curr : hexString.getChars()) // HEX[0-9] have charset range from 48 to 57 & HEX[A-F] have charset range from 65 to 70, hence for every hexchar in hexstring, returns the decimal value -> output is a list
{
Integer val = 0;
if(curr >= 65) //HEX['A'=>65, 'B'=>66, 'C'=>67, 'D'=>68, 'E'=>69, 'F'=>70]
val = curr - 65 + 10; //Decimal 10 = Hex A, ..... Decimal 15 = Hex F, hence added 10
else //HEX['0'=>48, '1'=>49, '2'=>50, '3'=>51, '4'=>52, '5'=>53, '6'=>54, '7'=>55, '8'=>56, '9'=>57]
val = curr - 48; //Decimal 0 = Hex 0, ..... Decimal 9 = Hex 9.
/* Since validFor uses trailing 0's to complete the validFor Binary string, hence if in case, for a Picklist let's say there are only 2 Controlling values, then
* Controlling Picklist Value will be residing in 01 & 10 ==> i.e 1000 & 0100 ==> baseCount will be either 0 or 1.
*/
if((val & 8) == 8) //Bitwise AND 1000 with val, if returns 1000 ==> Retrive the Controlling value from current baseCount position
{
//System.debug('Dependent Field: ' + currDependentValue.getValue() + ' Partof ControllingField:' + controllingValues[baseCount + 0].getValue());
fieldDependenciesData.get(controllingValues[baseCount + 0].getValue()).add(currDependentValue.getValue());
}
if((val & 4) == 4) //Bitwise AND 0100 with val, if returns 0100 ==> Retrive the Controlling value from current baseCount+1 positions ahead
{
//System.debug('Dependent Field: ' + currDependentValue.getValue() + ' Partof ControllingField:' + controllingValues[baseCount + 1].getValue());
fieldDependenciesData.get(controllingValues[baseCount + 1].getValue()).add(currDependentValue.getValue());
}
if((val & 2) == 2) //Bitwise AND 0010 with val, if returns 0010 ==> Retrive the Controlling value from current baseCount+2 positions ahead
{
//System.debug('Dependent Field: ' + currDependentValue.getValue() + ' Partof ControllingField:' + controllingValues[baseCount + 2].getValue());
fieldDependenciesData.get(controllingValues[baseCount + 2].getValue()).add(currDependentValue.getValue());
}
if((val & 1) == 1) //Bitwise AND 0001 with val, if returns 0001 ==> Retrive the Controlling value from current baseCount+3 positions ahead
{
//System.debug('Dependent Field: ' + currDependentValue.getValue() + ' Partof ControllingField:' + controllingValues[baseCount + 3].getValue());
fieldDependenciesData.get(controllingValues[baseCount + 3].getValue()).add(currDependentValue.getValue());
}
baseCount += 4;
}
}
for(String fieldDependenciesKey : fieldDependenciesData.keySet())
System.debug('fieldDependenciesData: '+fieldDependenciesKey+' :=: '+fieldDependenciesData.get(fieldDependenciesKey));
return new FieldDependencyWrapper(fieldDependenciesData, controllingFieldAPIValueVSLabel, dependentFieldAPIValueVSLabel);
}
A thorough understanding of the “validFor” field backend architecture is an advanced topic that is very valuable to Salesforce developers who have a knack for digging into the details and understanding the Salesforce Architecture & are aiming to become Salesforce Architects in their future.
In the process of writing this article, I also encountered a challenge, on how to Bulk upload field dependencies for both the controlling and dependent fields.
On performing quite a lot of searches, I found that People have been raising Salesforce Ideas to request for an easy way to Upload/Mass Update Field Dependency Matrix for fields.
Until Salesforce brings this as one of their New Features, there’s an Easy way to do this via Excel, VS Code and some basic knowledge of XMLs. You can follow the steps mentioned in this amazing Article below, to mass upload Picklist Dependency values.
Reference Docs:
I hope you enjoyed this article 📖 & found it insightful! 🔍!
Do hit the clap button👏 if you liked the article & leave a comment✍ if you have any queries/suggestions.
Enjoying the content? Don’t miss out on my future posts! Hit ‘Follow’ button to be the first to know about my new publications 🔔. Can’t wait to share more! 🚀🎉
Thanks for reading!