52

#Context# This question has been asked before in the past and I've found several discussions of it here on StackExchange and elsewhere (links below to pages I've found in my research).

So far I'm not satisfied with any of the answers I've seen, and wanted to create this question just to poll the crowd one more time in case anything has changed in recent history that now makes this solvable.

#The Problem# The basic challenge is easy to describe: You can use Schema Describe to obtain a list of picklist values for a given picklist field. That's not hard to do.

Where it gets tricky is if you have RecordType in the mix and the picklist is now being partially restricted based on the RecordType. If a field has say, 10 possible choices, but a RecordType restricts those choices down to 6, let's say, how do I know which six values are valid options for that field?

I want to obtain this information from inside an Apex class—this is the rub and what invalidates a lot of existing solutions to this problem.

#Possible Solutions#

  1. ###describeLayout()### Picklist value information seems to be readily available in the describeLayout() API call. Now if you've been around for a while you know people have gotten clever and learned to do a callout to the SF API from inside an Apex class but this feels too heavyweight for this purpose. Bringing in that API wrapper library and doing one of those self-calls just to know the right picklist values...there has to be a better way. (If you don't know what I'm talking about you can find it here: https://github.com/financialforcedev/apex-mdapi)
  2. ###<apex:inputField>### The inputField component is capable of all kinds of automagical stuff that isn't easily replicated, and this case is no exception. <apex:inputField> respects the RecordType and reduced set of values for the picklist field. Some folks have put together solutions that leverage this to reverse-engineer the list, in a way, by rendering inputField picklist selectors on a VisualForce page then pulling those values back off the HTML of the page. This is obviously a bit janky and requires the use of a VisualForce page.
  3. ###validFor?### I'm not even sure this is a possible approach, I'm fairly convinced it's a red herring at this point. A comment by the user Christophe Vidal on one of the questions in my research list below suggests that somehow validFor can be used to solve this problem. Some enterprising folks have figured out that even though validFor isn't documented as a property on PicklistEntry in Apex, you can extract it by serializing an instance to JSON and back. Then you have to chop it up using some bit manipulation to determine possible values. The purpose of "validFor" is to help people determine the connections between picklist values between controlling and dependent fields. I don't see how this can be extrapolated out to RecordTypes influencing picklists but if it's possible please do point me in the right direction.
  4. ###Hardcode it### Though I abhor this approach I feel it must be included to be fair and balanced. One strategy here that might work for your situation would be to just bake the subsets of values into your Apex. Or duplicate them somewhere like a CustomSetting and ask admins to maintain it alongside the RecordType restriction list.

#Conclusion# I've been casting about for a few days now looking for a clean solution to this problem and really haven't found one. Since I actually have to solve this problem I will eventually be forced into one of the above options unless a more palatable one is presented by someone here (here's hoping!).

Since in my use case a VisualForce page is indeed involved I think I will likely go the "apex:inputField" route. Save me from that terrible fate, good StackExchangers!

#Related Links and Discussions#

  1. https://stackoverflow.com/questions/7810801/how-to-describe-enumerate-picklist-enties-valid-for-a-specific-record-type-in
  2. https://developer.salesforce.com/forums?id=906F00000008z44IAA
  3. http://titancronus.com/blog/2014/07/03/acquiring-dependent-picklists-in-apex-contd/
  4. https://success.salesforce.com/ideaView?id=08730000000bhbwAAA
  5. https://developer.salesforce.com/forums/?id=906F00000008nNrIAI
  6. https://stackoverflow.com/questions/8560522/salesforce-record-type-and-opportunity-stage-relationship-database-location
  7. https://success.salesforce.com/ideaView?id=08730000000l6O5AAI
  8. https://github.com/financialforcedev/apex-mdapi/issues/4
3
  • 4
    +1 for one of the best written questions in a while (and the use of "janky"). Given that the answer is contained in the metadata, I'd go with option 1 but I'll be interested to see what others come up with. Yes, the package looks big but is incredibly useful. Option 4 is the real "janky" one
    – cropredy
    Commented Dec 31, 2015 at 1:23
  • 1
    I've personally used option (1). Besides a few trivial quirks (the name field for leads/contacts, for one), it's fast, and hardly noticeable on most layouts. Use that option if you just want to get it done like, this week. Other options may be possible, but I wouldn't trust (2) or (3) if they seem hackish, and option (4) is just asking for heartache. It'd be lovely if they had an official way to get at validFor that was documented.
    – sfdcfox
    Commented Dec 31, 2015 at 2:39
  • You definitely don't need an API wrapper (especially NOT the massively overkill FinancialForce wrapper) to make HTTP calls. I have implemented a solution based off the answer from @psun and it works quite well. Commented May 3, 2023 at 15:42

6 Answers 6

16

You can make use the User Interface API to get picklist values based on record type, https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_resources_picklist_values.htm

However, you need to make a REST API call and set Remote Site Settings. Here is the sample code for that,

Http http = new Http();
HttpRequest request = new HttpRequest();
String shost = System.Url.getSalesforceBaseURL().toExternalForm();
String url = shost + '/ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}/{fieldApiName}';
request.setEndpoint(url);
request.setMethod('GET');

// set Authorization 
request.setHeader('Authorization', 'OAuth '+UserInfo.getSessionId());
HttpResponse response = http.send(request);

You need to replace {objectApiName}, {recordTypeId} and {fieldApiName} with appropriate values. If anyone found a better solution not mentioned in the lists, please update.

1
3

I've been searching to try to solve this problem my self but found that the FinancialForce library in Github only works with API version 37 and earlier so is off limits now that we are on V50

Hopefully this approach will help others searching to try to solve the same issue

It is somewhat brute force but does mean you don't have to hardcode values, maintain custom metadata and if you are using LWC, it means you don't have to resort to VisualForce in order to get access to the apex:inputField

Here's a mechanism I've just built which seems to work

How it works:

  1. Use getDescribe to get active record types for your selected object
  2. Use getDescribe to get ALL the active picklist values for the field of interest
  3. Creates a list of records of your selected object using every possible combination of record type and picklist value. You need to ensure that any mandatory fields are populated here
  4. Creates a savepoint just in case the insert succeeds
  5. Tries to insert all the records. Salesforce prevents the insert if the picklist value is not valid for the associated recordtype
  6. For the combinations that succeed, builds a Map of lists of values The Map returns a list of permitted values when you supply the recordtypeID
  7. Rolls back the insert as you don't want junk in your database
global class getOnboardingValues {
    
    public static list<String> picklistValues(String object_name, String field_name) {
        list<String> values = new list<String>();
        list<String> types = new list<String>{object_name};
            list<Schema.DescribeSobjectResult> results = Schema.describeSObjects(types);
        for(Schema.DescribeSobjectResult res : results) {
            for (Schema.PicklistEntry entry : res.fields.getMap().get(field_name).getDescribe().getPicklistValues()) {
                if (entry.isActive()) {values.add(entry.getValue());}
            }
        }
        return values;
    }
    
    @AuraEnabled (cacheable=true)
    global static map<string, list<string>> getValuesbyRecType(){
        map<string, list<string>> retMap = new map<string, list<string>>();
        // Get valid record types for the Contact record
        Schema.DescribeSObjectResult R = Contact.SObjectType.getDescribe();
        List<Schema.RecordTypeInfo> RT = R.getRecordTypeInfos();
        
        // Get all allowed picklist values for the field
        list<String> allValues = picklistValues('Contact', 'Onboarding_Stage__c');
        
        // You need to know which fields are mandatory - in this case mandatory fields are
        // Contact.Lastname
        // Contact.Membership_Start_Date__c
        // 
        // Create a list of contacts with every combo of record type and picklist value
        
        List<contact> cList = new list<contact>(); // The list we will build with every record type / picklist value combo
        Map<ID,String> rtNameMap = new map<ID,String>(); // maps Record Type ID to friendly name
        Map<ID,String> rtDevNameMap = new map<ID,String>(); // maps Record Type ID to developer name
        for (Schema.RecordTypeInfo rti:RT){
            if (rti.isActive()) {
                for (string plv : allValues)
                    cList.add(new contact (lastname = rti.getDeveloperName() + ' ' + plv,
                                           recordtypeID = rti.getRecordtypeID(),
                                           Onboarding_Stage__c = plv,
                                           Membership_Start_Date__c = date.today()));
                rtNameMap.put(rti.getRecordtypeID(), rti.getName());
                rtDevNameMap.put(rti.getRecordtypeID(), rti.getDeveloperName());
            }
        }
        
        //Create a savepoint just in case all the inserts succeed
        Savepoint sp = Database.setSavepoint();
        // Try to insert all the values
        list<Database.SaveResult> srList = Database.insert(cList,FALSE); // allow partial save
        // Create a map of all records we tried to save
        map<ID, Contact> saveMap = new map<ID, Contact>();
        saveMap.putAll(cList);
        
        // Iterate through the results from the insert
        for (Database.SaveResult sr:srList){
            if (!sr.isSuccess()) {
                // log the failures to the debug log
                for(Database.Error err : sr.getErrors()) {
                    System.debug('The following error has occurred. ID: ' + sr.getID());                    
                    System.debug(err.getStatusCode() + ': ' + err.getMessage());
                    System.debug('Fields that affected this error: ' + err.getFields());
                }
            } else {
                // the insert succeeded so the values are OK. Add them to the retMap
                if (retMap.get(saveMap.get(sr.getId()).recordTypeID) == null)
                {
                    // we don;t have a list for this record type ID so instantiate a new list
                    retMap.put(saveMap.get(sr.getId()).recordTypeID, new List<String>());
                }
                // add the element to the list within the map entry
                retMap.get(saveMap.get(sr.getId()).recordTypeID).add(saveMap.get(sr.getId()).Onboarding_Stage__c);  
                // log the successful combination
                System.debug('Success. Record Type: ' + rtNameMap.get(saveMap.get(sr.getId()).recordTypeID) +
                             ' Picklist value: ' + saveMap.get(sr.getId()).Onboarding_Stage__c) ;
            }
        }
        // undo any saves that worked as we don;t want to pollute the database with the rubbish
        Database.rollback(sp);        
        return retMap;
    }
    
}
1
  • I imagine if you want this to be 100% generic you could retrieve the mandatory fields from the metadata API and just pass the object name and field name to the method Commented Nov 24, 2020 at 22:38
2

If you are using LWC then there is a wire adapter to save a lot of hassle

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reference_wire_adapters_picklist_values_record

Example below for the .js file:

import { LightningElement} from 'lwc';
import { getPicklistValues } from 'lightning/uiObjectInfoApi';
import YOUR_FIELD from '@salesforce/schema/ObjName.fieldName'; 
// in the line above, replace ObjName with the API name of the object and
// fieldName with the API name of the field you want to find value values for


export default class OnboardingStageParent extends LightningElement {

recordTypeID = '0126F00000130EEQAY' // your record type ID goes here
//if you are retrieving the value from a design attribute on the form then
//use @api recordTypeID here and replace recordTypeID in the wire
//with '$recordTypeID' including the single quotes

 @wire(getPicklistValues, { recordTypeId: '$recordTypeID', fieldApiName: YOUR_FIELD })
   getOptions({error, data}) {
   if (data) {
     // process the data returned.  data.values holds the legal values
        } else if (error) {
            console.log(error);
        } else {
            console.log('No data and no error. RecTypeID = ' + this.recordTypeID);
        }
    }
}
1
  • 1
    js example added Commented Dec 6, 2020 at 21:45
1

This method actually works well - it is a little codey up front but this can be put into a utility module and modified as you see fit but it is a workable solution if you pass in parameters to fit the recordType name value... I've implemented a similar instance in a utility class to do this and it uses "validFor" to get dependent values - which RecordType fits that

Get lists of dependent picklist options in Apex

1
  • I'd give this a upvote if you added more context / example to your approach. it seems very exciting. Commented Feb 24, 2018 at 15:13
1

If I had to do this, I would create a custom setting that contains two fields: the record type name (or developername, whatever you prefer), and the picklist value. This would allow you to store each picklist value with an associated record type name, and it is easily updated if picklist values change. Then in the apex code you can use standard custom setting methods to pull back all the picklist values that are associated with a specific record type name. This avoids hard-coding of picklist values in your apex class and also avoids tons of extra code in your class. This isn't perfect, but for expediency and simplicity sake, I would do it this way if I only had a couple of picklists where it was important for me to only get the values associated with a certain record type. This would work fine on a small scale. If for some reason you have a lot of code that is looking for multiple picklist values for recordtypes on multiple objects, this would probably not be feasible from a maintainability standpoint.

0

while finding the solution for the same query i found this link it really solves the issue. I hope this works for everyone.

https://github.com/pozil/picklist-utils/blob/main/src/main/default/classes/PicklistUtils.cls

mark this best answer

2
  • 1
    Link-only answers are not great. The link might become invalid anytime and even if not - the user still needs to extract the interesting bits from the referenced page. It's much nicer to put the key learnings from the page into one's answer. Commented Feb 8 at 10:57
  • Thanks @FelixvanHove I will try to post the answers from now one in a more appropriate way.
    – Raj Singh
    Commented Apr 19 at 10:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .