Dependent Picklist Values in Apex - A Concise Solution

I don't want to read the explanation - just take me to the code!

The Problem

Salesforce supports the concept of a "dependent" picklist, that is, a picklist whose values vary based on the state of another field (the "controlling" field).  According to Salesforce Help:

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.

Figure 1 shows a field dependency defined between a controlling picklist field, Control, and the dependent picklist field, Depend.  For each of the possible values of Control (A, B, C, D, E, F, G, H), the columns of the table show in yellow the values of Depend (X, Y, Z) that are valid (or "included").
Figure 1) Field dependency defined between "Control" and "Depend" fields.

Figure 2 shows a field dependency defined between a controlling checkbox field, CheckBox, and a dependent picklist field, DependOnCheckBox.  In the case of a checkbox field, there are exactly two possible controlling values, "Unchecked" and "Checked".  These correspond to the Boolean values false and true in Apex.

Figure 2) Field dependency defined between "CheckBox" and "DependOnCheckBox" fields.

It has been a perennial problem for Force.com / Lightning Platform developers to determine in Apex code which picklist values are associated with a given controlling field state.  The Apex class, Schema.DescribeFieldResult, has a method, getPicklistValues() that returns a list of Schema.PicklistEntry objects, representing all possible picklist values for a field, including inactive ones.  There is no method to get only the values that correspond to a particular controlling field value.  (There is an idea, originally suggested in 2012, to add such a method.  As of this writing, it remains unimplemented.  Upvote the idea here.)

The Salesforce SOAP API Developer Guide describes how to get this information using the API.  When obtained from the API, the PicklistEntry object contains a member, called validFor, that encodes the information we seek.  Some very clever people discovered long ago that the Apex Schema.PicklistEntry object also contains this member, even though there is no method to return its value.

Figure 3 shows the result of serializing Schema.PicklistEntry objects into JSON strings, thereby revealing the presence of the validFor member.

Figure 3) Serialized PicklistEntries reveal the "validFor" attribute.

The value in the validFor member is a Base64-encoded bitmap.  Each bit in the bitmap indicates whether this dependent picklist value is "valid for" a corresponding controlling field value.  An excellent article on Base64 can be found on Wikipedia; but the short explanation is that each character in a Base64 string encodes 6 bits of information (because 64 = 26).  Figure 4 illustrates how this encoding is done for the "Z" value of the Depend field.  Notice that this result is the same as the first two characters in the validFor field for "Z" in Figure 3.
Figure 4) Encoding field dependencies using Base64.

The validFor fields encode the rows of the field dependency table.  We can write code that extracts the validFor string and decodes it, then use the bits to determine for which controlling values each dependent value is valid.  But we really want the columns of the field dependency table — we want to know which dependent values are valid for each controlling value.  The code must invert the table and return results for each controlling value.

There have been several solutions to this problem posted over the years.  Most of these use two or more classes and some complicated logic.  They get the job done, but I thought there must be a simpler, more concise way to achieve the same result.  The code below is a single method that takes the Schema.SObjectField token of the dependent picklist field, and returns a map, the keys of which are the possible values of the controlling field, and the values of which are lists of valid dependent field values.  Note that inactive picklist values are excluded from both controlling and dependent fields.  All of this in just 30 lines of code*.  Read on for a more detailed explanation of how the code works.

The Solution

public static Map<Object,List<String>> getDependentPicklistValues( Schema.sObjectField dependToken )
{
    Schema.DescribeFieldResult depend = dependToken.getDescribe();
    Schema.sObjectField controlToken = depend.getController();
    if ( controlToken == null ) return null;
    Schema.DescribeFieldResult control = controlToken.getDescribe();
    List<Schema.PicklistEntry> controlEntries =
    (   control.getType() == Schema.DisplayType.Boolean
    ?   null
    :   control.getPicklistValues()
    );

    String base64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    Map<Object,List<String>> dependentPicklistValues = new Map<Object,List<String>>();
    for ( Schema.PicklistEntry entry : depend.getPicklistValues() ) if ( entry.isActive() )
    {
        List<String> base64chars =
            String.valueOf
            (   ((Map<String,Object>) JSON.deserializeUntyped( JSON.serialize( entry ) )).get( 'validFor' )
            ).split( '' );
        for ( Integer index = 0; index < (controlEntries != null ? controlEntries.size() : 2); index++ )
        {
            Object controlValue =
            (   controlEntries == null
            ?   (Object) (index == 1)
            :   (Object) (controlEntries[ index ].isActive() ? controlEntries[ index ].getLabel() : null)
            );
            Integer bitIndex = index / 6, bitShift = 5 - Math.mod( index, 6 );
            if  (   controlValue == null
                ||  (base64map.indexOf( base64chars[ bitIndex ] ) & (1 << bitShift)) == 0
                ) continue;
            if ( !dependentPicklistValues.containsKey( controlValue ) )
            {
                dependentPicklistValues.put( controlValue, new List<String>() );
            }
            dependentPicklistValues.get( controlValue ).add( entry.getLabel() );
        }
    }
    return dependentPicklistValues;
}

Usage

If you know the object and dependent picklist field at the time you're writing your code, you can invoke the getDependentPicklistValues method this way:
Map<Object,List<String>> dependValuesByControlValue = getDependentPicklistValues( Account.Depend__c );
If you need to determine the object and/or dependent picklist field dynamically, you can overload getDependentPicklistValues like this:
public static Map<Object,List<String>> getDependentPicklistValues( String sObjectName, String fieldName )
{
    return getDependentPicklistValues
    (   Schema.getGlobalDescribe().get( sObjectName ).getDescribe().fields.getMap().get( fieldName )
    );
}
Figures 5 and 6 show the output of the method for both picklist and checkbox controlling fields:

Figure 5) Final output maps Control values to Depend values.

Figure 6) Dependent values mapped to Boolean controlling values.

Analysis

public static Map<Object,List<String>> getDependentPicklistValues( Schema.sObjectField dependToken )
{
    Schema.DescribeFieldResult depend = dependToken.getDescribe();
    Schema.sObjectField controlToken = depend.getController();
    if ( controlToken == null ) return null;
The first part of the code confirms that the requested field is actually a dependent picklist.  It assumes that dependToken is not null, and if it is null, the method will throw a NullPointerException.  It gets the token of the controlling field; and if that's null, the method returns null.  The controlling field token will be non-null only if the requested field is a dependent picklist.
    Schema.DescribeFieldResult control = controlToken.getDescribe();
    List<Schema.PicklistEntry> controlEntries =
    (   control.getType() == Schema.DisplayType.Boolean
    ?   null
    :   control.getPicklistValues()
    );
If the controlling field is not a checkbox, the code gets its list of Schema.PicklistEntry objects.  If the controlling field is a checkbox, the controlEntries variable is set to null.  Later in the code, this will be used to test whether the controlling field is a checkbox or not.
    String base64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    Map<Object,List<String>> dependentPicklistValues = new Map<Object,List<String>>();
    for ( Schema.PicklistEntry entry : depend.getPicklistValues() ) if ( entry.isActive() )
    {
The base64map string will be used to decode the characters of the validFor fields.  Each character is in the position of the number it represents.  So, "A" is at index 0 within the string, and "A" represents the decoded value 0, "B" represents the value 1, and so on up to "/", which is the encoding of 63.

The map of lists, dependentPicklistValues, will contain the results of the method.  Each list in the map represents a column of the field dependency matrix, keyed by the controlling field value (the heading of the column).

The for-if-loop on line 15 iterates over the active dependent picklist field values (the rows of the field dependency matrix).  When this loop completes, the method will be finished.
        List<String> base64chars =
            String.valueOf
            (   ((Map<String,Object>) JSON.deserializeUntyped( JSON.serialize( entry ) )).get( 'validFor' )
            ).split( '' );
Lines 17-20 are a single statement that does a lot of work.  The Schema.PicklistEntry is serialized and deserialized using the Apex JSON class.  By using JSON.deserializeUntyped, we avoid the need to declare a class to use when deserializing, and the code will continue to work even if Salesforce changes the internals of the Schema.PicklistEntry class — as long as it continues to have a validFor field.

After deserializing, the code extracts the validFor field and converts its value to a String.  (It already is a String, but the get method returns an Object.  We could cast it as a String, but String.valueOf is safer, and reads better, IMO.)

Finally, the code splits the Base64 string into its individual characters.  Each String in base64chars is a single character encoding six columns of the field dependency matrix for this dependent picklist value.
        for ( Integer index = 0; index < (controlEntries != null ? controlEntries.size() : 2); index++ )
        {
The inner for-loop iterates over the columns of the field dependency matrix.  If the controlling field is a picklist, then the loop iterates over the controlling picklist entries.  If the controlling field is a checkbox, the loop iterates over two values — false and true.
            Object controlValue =
            (   controlEntries == null
            ?   (Object) (index == 1)
            :   (Object) (controlEntries[ index ].isActive() ? controlEntries[ index ].getLabel() : null)
            );
Lines 23-27 are another single statement that does a lot.  If the controlling field is a picklist, then controlValue will be assigned the label of the active picklist value, or null, if the picklist value is inactive.  If the controlling field is a checkbox, then controlValue will be assigned false (when index is 0) or true (when index is 1).
            Integer bitIndex = index / 6, bitShift = 5 - Math.mod( index, 6 );
            if  (   controlValue == null
                || (base64map.indexOf( base64chars[ bitIndex ] ) & (1 << bitShift)) == 0
                ) continue;
Line 28 computes the location of this column's bit in the validFor bitmap.  bitIndex identifies which of the Base64 characters contains the bit, and bitShift identifies which of the six bits we want from the decoded character.

If the controlling picklist value is inactive or if the decoded bit is a zero, the code skips this column.  The decoding is done using the indexOf method, which converts the Base64 character into the value (0-63) that it represents.
            if ( !dependentPicklistValues.containsKey( controlValue ) )
            {
                dependentPicklistValues.put( controlValue, new List<String>() );
            }
            dependentPicklistValues.get( controlValue ).add( entry.getLabel() );
When execution gets here, the dependent picklist value is valid for the controlling field value.  This code segment inserts the dependent picklist value into the list that corresponds to the controlling field value.  This effectively inverts the field dependency matrix so that the method can return the dependency information in a useful form.
        }
    }
    return dependentPicklistValues;
}
The inner and outer loops complete, and we have the final result.  As long as the requested field is a dependent picklist field, the method will never return null.  The map returned could be empty, however; and if a controlling field value has no valid dependent picklist values, the map will not contain an entry for that controlling field value.  Be sure to test what you get out of the map for null before treating it as a list.  If you get a list from the map, it will always have at least one dependent picklist value in it.

Testing Considerations

Because the code works with metadata, and not all Salesforce orgs have standard dependent picklists (only those with State and Country Picklists enabled would), I am unable to offer a complete test class that would run as-is.  Any test will depend on custom fields defined in that specific org.

Testing with a custom controlling picklist / dependent picklist pair should provide 100% coverage, as long as not all dependent picklist values are valid for every controlling field value.  Complete functional testing should include a case where a controlling picklist value has no corresponding dependent field values, where some controlling and dependent values are inactive, and a case of a controlling checkbox field.

You might consider creating three custom fields expressly for this purpose.  For example, on Account, create three fields, a checkbox field called Field_Dependency_Test_A__c, and two picklist fields called Field_Dependency_Test_B__c and Field_Dependency_Test_C__c.  Restrict the field-level security of these fields only to profiles that are able to run Apex tests (System Administrator and anyone else that can deploy code or manage change sets).  Don't add these fields to any page layouts.  They should remain unpopulated in any records in the database.

Configure the picklist values as shown in Figure 7 and field dependencies as shown in Figure 8.

Figure 7) Picklist values for Field_Dependency_Test_B__c (left) and Field_Dependency_Test_C__c (right).  Note the inactive values.

Figure 8) Field dependencies for the Field_Dependency_Test_X__c fields.

With those three custom fields in place, you can use the following test class to get 100% coverage and full functional testing.  Replace the class name, YourClassNameHere, with the name of the class that contains the getDependentPicklistValues method.
@isTest
private class TestGetDependentPicklistValues
{
    @isTest
    private static void testBooleanController()
    {
        Test.startTest();
        Map<Object,List<String>> result =
            YourClassNameHere.getDependentPicklistValues( Account.Field_Dependency_Test_B__c );
        Test.stopTest();
        List<String> resultList = result.get( false );
        System.assert( resultList != null );
        System.assert( resultList.size() == 1 );
        System.assert( resultList[0] == 'When False' );
        resultList = result.get( true );
        System.assert( resultList != null );
        System.assert( resultList.size() == 1 );
        System.assert( resultList[0] == 'When True' );
    }

    @isTest
    private static void testPicklistController()
    {
        Test.startTest();
        Map<Object,List<String>> result =
            YourClassNameHere.getDependentPicklistValues( Account.Field_Dependency_Test_C__c );
        Test.stopTest();
        List<String> resultList = result.get( 'When False' );
        System.assert( resultList != null );
        System.assert( resultList.size() == 1 );
        System.assert( resultList[0] == 'Ugly' );
        resultList = result.get( 'When True' );
        System.assert( resultList != null );
        System.assert( resultList.size() == 2 );
        System.assert( resultList[0] == 'Good' );
        System.assert( resultList[1] == 'Bad' );
        resultList = result.get( 'Unused' );
        System.assert( resultList == null );
    }
}

Conclusion

Unless and until Salesforce provides native support in Apex to retrieve dependent picklist information, developers will need to continue using solutions like this one.  In my research, I have yet to find a more concise, light-weight solution than this one; and I'm very happy to offer it to the community.  Please let me know if you have any questions, find any bugs, or have any suggestions for improvements.

Share this blog with your friends and colleagues and follow me on Twitter @GlynAtSlalom.

*As reported by the Developer Console Code Coverage pane.

Comments

  1. Hi,

    I'd like to add that when working with API names of picklists, you should use the API name instead of labels.
    i.e. instead of controlEntries[ index ].getLabel() use controlEntries[ index ].getValue()

    just my 2 cents.
    Thanks for the hard work writing this blog post.

    ReplyDelete
  2. Hi, I have made some update based on your code, so that some bugs can be avoided.

    public static Map> getDependentPicklistValues( Schema.sObjectField dependToken ){
    Schema.DescribeFieldResult depend = dependToken.getDescribe();
    // System.debug('5 depend::::'+depend.getPicklistValues());
    Schema.sObjectField controlToken = depend.getController();
    if ( controlToken == null ) return null;
    Schema.DescribeFieldResult control = controlToken.getDescribe();
    List controlEntries =
    ( control.getType() == Schema.DisplayType.Boolean
    ? null
    : control.getPicklistValues()
    );

    if(controlEntries.isEmpty()) {
    throw new MyException('This Field Don\'t Have Controlling Field!');
    }

    String base64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    Map> dependentPicklistValues = new Map>();
    for ( Schema.PicklistEntry entry : depend.getPicklistValues() ){
    if ( entry.isActive() ){
    List base64chars =
    String.valueOf
    ( ((Map) JSON.deserializeUntyped( JSON.serialize( entry ) )).get( 'validFor' )
    ).split( '' );
    if(base64chars.isEmpty()){
    throw new MyException('This Picklist Value Don\'t Have Controlling Value!');
    }
    for ( Integer index = 0; index < (controlEntries != null ? controlEntries.size() : 2); index++ )
    {
    Object controlValue =
    ( controlEntries == null
    ? (Object) (index == 1)
    : (Object) (controlEntries[ index ].isActive() ? controlEntries[ index ].getValue() : null)
    );
    Integer bitIndex = index / 6, bitShift = 5 - Math.mod( index, 6 );

    if(bitIndex >= base64chars.size()) continue;

    if ( controlValue == null
    || (base64map.indexOf( base64chars[ bitIndex ] ) & (1 << bitShift)) == 0
    ) continue;
    if ( !dependentPicklistValues.containsKey( String.valueOf(controlValue )) )
    {
    dependentPicklistValues.put( String.valueOf(controlValue), new List() );
    }
    dependentPicklistValues.get( String.valueOf(controlValue) ).add( entry.getValue() );
    }
    }
    }
    return dependentPicklistValues;
    }

    public class MyException extends Exception {}

    ReplyDelete
    Replies
    1. hi
      please publish simple code on schema.Dependent Picklist

      Delete
  3. Hi Sir,

    I am facing an issue in line no 30. i.e. || (base64map.indexOf( base64chars[ bitIndex ] ) & (1 << bitShift)) == 0

    Its giving me list index out of bounds. Any thoughts?

    I am trying to use the same code

    Prior thanks !!

    ReplyDelete
  4. Thanks, you gave me an extra hour to enjoy life :P

    I made some changes to use values instead of labels and to return the entire PicklistEntry.

    public static Map> getDependentPicklistValues(Schema.sObjectField dependToken) {
    Schema.DescribeFieldResult depend = dependToken.getDescribe();
    Schema.sObjectField controlToken = depend.getController();
    if (controlToken == null) return null;
    Schema.DescribeFieldResult control = controlToken.getDescribe();
    List controlEntries = (control.getType() == Schema.DisplayType.Boolean ? null : control.getPicklistValues());
    String base64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    Map> dependentPicklistValues = new Map>();
    for (Schema.PicklistEntry entry : depend.getPicklistValues()) if (entry.isActive()){
    List base64chars = String.valueOf(((Map) JSON.deserializeUntyped(JSON.serialize(entry))).get('validFor')).split('');
    for (Integer index = 0; index < (controlEntries != null ? controlEntries.size() : 2); index++) {
    Object controlValue = (controlEntries == null ? (Object) (index == 1) : (Object) (controlEntries[ index ].isActive() ? controlEntries[ index ].getValue() : null));
    Integer bitIndex = index / 6, bitShift = 5 - Math.mod(index, 6);
    if (controlValue == null || (base64map.indexOf(base64chars[ bitIndex ]) & (1 << bitShift)) == 0) continue;
    if (!dependentPicklistValues.containsKey(controlValue))
    dependentPicklistValues.put( controlValue, new List() );
    dependentPicklistValues.get( controlValue ).add( entry );
    }
    }
    return dependentPicklistValues;
    }

    public static Map> getDependentPicklistValues(String sObjectName, String fieldName) {
    return getDependentPicklistValues(Schema.getGlobalDescribe().get(sObjectName).getDescribe().fields.getMap().get(fieldName));
    }

    ReplyDelete
  5. This is not working on the standard Mailing State Code picklist values.
    I am trying to retrieve the dependent picklist values for Contact.MailingStateCode

    It returning the error List index out of bounds at this line

    (base64map.indexOf( base64chars[ bitIndex ] ) & (1 << bitShift)) == 0

    any idea?

    ReplyDelete
  6. If you are encountering an exception using this solution with State and Country picklists, please read on.

    First, thanks for sharing and explaining your solution. When working with state and country picklists the length of the validFor string is variable, not fixed by the number of controlling entries.

    Here's the pick list entry info for the first 10 states to demonstrate this :

    Label : Aceh, Value : AC, Active : true, Default : false, validFor String length: 20, validFor : AAAAAAAAAAAAAAAAAQAA
    Label : Acre, Value : AC, Active : true, Default : false, validFor String length: 8, validFor : AAAAAQAA
    Label : Agrigento, Value : AG, Active : true, Default : false, validFor String length: 20, validFor : AAAAAAAAAAAAAAAAAAQA
    Label : Aguascalientes, Value : AG, Active : true, Default : false, validFor String length: 24, validFor : AAAAAAAAAAAAAAAAAAAAAAAB
    Label : Alabama, Value : AL, Active : true, Default : false, validFor String length: 40, validFor : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ
    Label : Alagoas, Value : AL, Active : true, Default : false, validFor String length: 8, validFor : AAAAAQAA
    Label : Alaska, Value : AK, Active : true, Default : false, validFor String length: 40, validFor : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ
    Label : Alberta, Value : AB, Active : true, Default : false, validFor String length: 8, validFor : AAAAAAEA
    Label : Alessandria, Value : AL, Active : true, Default : false, validFor String length: 20, validFor : AAAAAAAAAAAAAAAAAAQA
    Label : Amapá, Value : AP, Active : true, Default : false, validFor String length: 8, validFor : AAAAAQAA

    There are different ways to handle this. I choose to change line 28 from

    Integer bitIndex = index / 6, bitShift = 5 - Math.mod( index, 6 );

    to

    Integer bitIndex = index / 6;
    if (bitIndex > base64chars.size()-1) { break; }
    Integer bitShift = 5 - Math.mod( index, 6 );

    You could also change the exit condition of the loop to use the length of the validFor string instead of the number of controlling field values.

    Thanks again for sharing!

    ReplyDelete
    Replies
    1. Excellent work sir! I might never have figured this out. All the bit shifting is a little crazy to me lol

      Delete
  7. Thank you so much for this explanation...very helpfull

    ReplyDelete
  8. This is a fine bit of coding. Why Salesforce hasn't implemented some platform functions to do this is beyond me.

    ReplyDelete
  9. what if? if we use controlling field as checkbox and i want get based option selected checkbox checked or unchecked.

    ReplyDelete
  10. Hi Sir , I am getting List Index Out Of Bounds :4 Error , can u please help resolve this .

    Thanks in Advance

    ReplyDelete
  11. Nice work guys!

    I would like to add UI-API reference [here|https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_features_records_dependent_picklist.htm]

    I hope this post might help other in future so one can find the best possible solution require for the task

    Once again thank you all for sharing your thoughts

    ReplyDelete
  12. Don't know if you have seen this issue or not. I experienced it when the dependent entry only had a controlling near the front of the array. I've included what Salesforce is returning for the describe call. Notice that validFor is (now) variable in length and can extend to accommodate the 300 (max) controlling fields. When I have a lot of controlling fields, but the controlling field definition is completely satisfied, Salesforce provides a truncated version of validFor. I don't know if this is a change by Salesforce, but the code is assuming that the length of validFor is always aligns with the number of control entries (for loop in 21 above). I ended up putting a quick fix in to break the inner loop if bitindex exceeded the length of base64chars, but you should be able to modify the for loop criteria and keep the line count down.

    Thanks for keeping this piece of code out here. I always liked this version.

    {
    "active": true,
    "defaultValue": false,
    "label": "10 fxyzzy",
    "validFor": "AAAAAAAAAAAB",
    "value": "10 fxyzzy"
    },
    {
    "active": true,
    "defaultValue": false,
    "label": "10 gadzooks",
    "validFor": "IAAA",
    "value": "10 gadzooks"
    },
    {
    "active": true,
    "defaultValue": false,
    "label": "10 just another entry",
    "validFor": "AAAAAAAAAAAAAAAABAAA",
    "value": "10 just another entry"
    },


    ReplyDelete
  13. 6 years later and this code saved me!
    I did some modification for my specific use case and now it works perfectly.

    Thanks!!

    ReplyDelete

  14. Transform your CRM with a top Salesforce consultant. We provide tailored solutions for streamlined workflows, enhanced data management, and optimized sales processes. Maximize efficiency and drive growth with our expert Salesforce consulting services.

    ReplyDelete

Post a Comment

Popular posts from this blog

Ask Me A Question or Suggest A Blog Topic