What’s more annoying than a picklist? A Global Value Set. What’s more annoying than a Global Value Set? A Global Value Set that requires regular maintenance.
But you can update picklists and Global Value Sets in Visual Studio Code, adding, removing (actually, deactivating), repositioning and sorting values as you wish, you might intervene now. All correct, but what if picklist values are determined from a, say, third party system and you simply don’t have a working student to keep the values updated on a, say, hourly base?
That’s where the beloved Metadata API comes in handy. Below you’ll find a collection of a total of 7 APEXREST enabled classes, which enable you to remote control available values in your local and global picklist. All you need as a prerequisite are
- Salesforce’s MetadataService class (don’t forget to add the MetadataServiceTest class when productionizing – you won’t need all classes though), and
- a custom PicklistHelper class, a collection of user-friendly methods abstracted from MetadataService.
Here comes the overview of all available classes – one for each operation:
- GlobalValueSetActivateService
- GlobalValueSetDeactivateService
- GlobalValueSetUpdateService
- PicklistActivateService
- PicklistAddToRecordTypeService
- PicklistDeactivateService
- PicklistUpdateService
As you see, it’s still not possible to delete picklist or global values – while Salesforce does not state this officially anywhere, there is simply no such operation available in the Metadata API. Deactivating a picklist value remains the highest of highs for the time being.
Class collection
PicklistHelper
public without sharing class PicklistHelper {
/**
* @description Initializes a MetadataService.MetadataPort and populates it with the user session ID, default session headers and a custom configured timeout
* @return A configured MetadataService.MetadataPort
*/
public static MetadataService.MetadataPort createService(){
MetadataService.MetadataPort service = new MetadataService.MetadataPort();
service.SessionHeader = new MetadataService.SessionHeader_element();
service.SessionHeader.sessionId = UserInfo.getSessionId();
service.timeout_x=120000;
return service;
}
/**
* @description Retrieves Picklist Values for a Record Type
* @param sObjectName String; API Name of sObject
* @param fieldName String; API Name of Field
* @return List<Schema.PicklistEntry> Picklist Values of the specified field
*/
public static List<Schema.PicklistEntry> retrievePicklistData(String sObjectName, String fieldName){
// Prepare the values of the picklist field
List<Schema.PicklistEntry> ple = Schema.getGlobalDescribe()?.get(sObjectName)?.getDescribe()?.fields.getMap()?.get(fieldName)?.getDescribe().getPicklistValues();
return ple;
}
/**
* @description Constructs an empty Custom Field of the Picklist Type to be populated in the Service classes
* @param sObjectName String; API Name of sObject
* @param fieldName String; API Name of Field
* @return MetadataService.CustomField Custom Field Definition
*/
public static MetadataService.CustomField createCustomField(String sObjectName, String fieldName){
MetadataService.CustomField customField = new MetadataService.CustomField();
customField.fullName = sObjectName + '.' + fieldName;
customField.label = Schema.getGlobalDescribe()?.get(sObjectName)?.getDescribe()?.fields.getMap()?.get(fieldName)?.getDescribe().getLabel();
customField.type_x = 'Picklist';
return customField;
}
/**
* @description Saves specified Metadata, with options to notify admins on Success
* @param service MetadataService.MetadataPort; An open Metadata Port
* @param obj MetadataService.Metadata; prepared Metadata to save
* @param notify Boolean; whether to notify Administrators
* @param notificationMessage String; message to notify Administrators
* @return Boolean: true for success; false for error (save errors)
*/
public static Boolean saveMetadata(MetadataService.MetadataPort service, MetadataService.Metadata obj, Boolean notify, String notificationMessage){
List<MetadataService.SaveResult> lstResults = service.updateMetadata( new List<MetadataService.Metadata>{ obj });
if(lstResults != null && lstResults.size() > 0){
for(MetadataService.SaveResult result : lstResults) {
if(result.errors != null) {
// Error logging
ErrorLog__c errorLog = new ErrorLog__c(
ErrorBody__c = 'Metadata Save Error. First Error: ' + result.errors.toString(),
RequestBody__c = 'Request: ' + obj.toString(),
Process__c = 'Metadata Insert'
);
insert errorLog;
return false;
}
}
if(notify){
NotifyAdminController.sendNotification('Metadata updated!', notificationMessage, null);
}
return true;
}
// Error logging
ErrorLog__c errorLog = new ErrorLog__c(
ErrorBody__c = 'Metadata Save Error. No results',
RequestBody__c = 'Request: ' + obj.toString(),
Process__c = 'Metadata Insert'
);
insert errorLog;
return false;
}
/**
* @description Apply a Value Set to a Custom Field (unsorted)
* @param customField MetadataService.CustomField; Custom Field definition
* @param newValues MetadataService.CustomValue; List of Values to apply to Custom Field
* @param sortMe Boolean; with alpha sorting = true; without alpha sorting = false
* @return MetadataService.CustomField Manipulated (but not yet saved) Custom Field
*/
public static MetadataService.CustomField applyNewValueSet(MetadataService.CustomField customField, List<MetadataService.CustomValue> newValues, Boolean sorted){
customField.valueSet = new MetadataService.ValueSet();
MetadataService.ValueSetValuesDefinition vd = new MetadataService.ValueSetValuesDefinition();
vd.sorted = sorted;
vd.value = newValues;
customField.valueSet.valueSetDefinition = vd;
return customField;
}
}
GlobalValueSetActivateService
@RestResource(urlMapping='/GlobalValueSetActivateService/*')
global without sharing class GlobalValueSetActivateService{
/**
* @routing /GlobalValueSetActivateService
* @description Activates a value on a Global Value Set. newValue does not necessarily have to exist on the Global Value Set,
* so this can also be used to create and immediately activate a value on a Global Value Set.
* @caution If any field on the Global Value Set is on an object which uses Record Types, the Picklist Value needs to be added
* to applicable Record Types via PicklistValueRecordTypeService
* @param valueSetName String; API Name of the Global Value Set
* @param newValue String; API Name of the old value
* @param newLabel String; it's possible to give the new value a different label; usually just set the same value as for newValue
* @param position Integer; at which position the new value should be inserted
* @param isDefault Boolean; whether the activated value should be set as the Global Value Set's Default Value
* @return Boolean; success = true; failure = false
* Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
*/
@HttpPost
global static Boolean activateGlobalValue(String valueSetName, String newValue, String newLabel, Integer position, Boolean isDefault, Boolean sortMe) {
// Creates Service and retrieves Global Value Set values
MetadataService.MetadataPort service = PicklistHelper.createService();
MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
if(readResult != null){
MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
// Create a new list of values
List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
// Loop through values retrieved from Global Value Set and let a counter run
// Fill the new list with retrieved values unless the retrieved value is the one to activate
// If the counter is at the desired position, put the new value here and set it to active
if(globalValueSet.customValue != null){
Integer i = 0;
for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
i++;
if(i == position){
MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
newCustomValue.fullName = newValue;
newCustomValue.label = newLabel;
newCustomValue.default_x = isDefault;
newCustomValue.isActive = true;
newValues.add(newCustomValue);
}
if(customValue.fullName != newValue){
MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
newCustomValue.fullName = customValue.fullName;
newCustomValue.label = customValue.label;
newCustomValue.default_x = !isDefault ? customValue.default_x : false;
newCustomValue.isActive = customValue.fullName == newValue ? true : customValue.isActive;
newValues.add(newCustomValue);
}
}
// Apply new Values to Global Value Set
globalValueSet.customValue = newValues;
globalValueSet.sorted = sortMe;
// Save changed Metadata
Boolean success = PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value activated on Global Value Set: ' + valueSetName + ' and value: ' + newValue + '.');
return success;
}
return false;
}
return false;
}
}
GlobalValueSetDeactivateService
@RestResource(urlMapping='/GlobalValueSetDeactivateService/*')
global without sharing class GlobalValueSetDeactivateService{
/**
* @routing /GlobalValueSetDeactivateService
* @description Deactivates a value on a Global Value Set. If oldValue does not exist on the Global Value Set, no change is made.
* @param valueSetName String; API Name of the Global Value Set
* @param oldValue String; API Name of the value to deactivate
* @return Boolean; success = true; failure = false
* Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
*/
@HttpPost
global static Boolean deactivateGlobalValue(String valueSetName, String oldValue, Boolean sortMe) {
// Creates Service and retrieves Global Value Set values
MetadataService.MetadataPort service = PicklistHelper.createService();
MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
if(readResult != null){
MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
// Create a new list of values
List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
// Loop through values retrieved from Global Value Set and let a counter run
// If value is not the value to deactivate, put the value into the new list
if(globalValueSet.customValue != null){
for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
newCustomValue.fullName = customValue.fullName;
newCustomValue.label = customValue.label;
newCustomValue.default_x = customValue.default_x;
newCustomValue.isActive = customValue.fullName == oldValue ? false : customValue.isActive;
newValues.add(newCustomValue);
}
// Apply new Values to Global Value Set
globalValueSet.customValue = newValues;
globalValueSet.sorted = sortMe;
// Save changed Metadata
return PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value deactivated on Global Value Set: ' + valueSetName + ' and value: ' + oldValue + '.');
}
return false;
}
return false;
}
}
GlobalValueSetUpdateService
@RestResource(urlMapping='/GlobalValueSetUpdateService/*')
global without sharing class GlobalValueSetUpdateService{
/**
* @routing /GlobalValueSetUpdateService
* @description Update a picklist value. Technically it consists of deactivating the old value, and entering another value at the same position.
* Records carrying the old value will not be changed.
* @param valueSetName String; API Name of the Global Value Set
* @param newValue String; API Name of the new value
* @param newLabel String; Label of the new value; usually same as newValue
* @param oldValue String; API Name of the old value
* @param isDefault Boolean; whether the new value should be set as the Global Value Set's Default Value
* @return Boolean; success = true; failure = false
* Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
*/
@HttpPost
global static Boolean updateGlobalValue(String valueSetName, String newValue, String newLabel, String oldValue, Boolean isDefault, Boolean sortMe) {
// Creates Service and retrieves Global Value Set values
MetadataService.MetadataPort service = PicklistHelper.createService();
MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
if(readResult != null){
MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
// Create a new list of values
List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
// Loop through values retrieved from Global Value Set
// Stop execution if the value to rename to is already part of the Value Set
// Otherwise put the looped value into the new list - unmanipulated if it's not the value to update, manipulated otherwise
if(globalValueSet.customValue != null){
for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
newCustomValue.isActive = customValue.isActive;
if(customValue.fullName == newValue){
return false;
} else if(customValue.fullName == oldValue){
newCustomValue.fullName = newValue;
newCustomValue.label = newLabel;
newCustomValue.default_x = isDefault;
} else {
newCustomValue.fullName = customValue.fullName;
newCustomValue.label = customValue.label;
newCustomValue.default_x = !isDefault ? customValue.default_x : false;
}
newValues.add(newCustomValue);
}
// Apply new Values to Global Value Set
globalValueSet.customValue = newValues;
globalValueSet.sorted = sortMe;
// Save changed Metadata
return PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value updated on Global Value Set: ' + valueSetName + ' from value: ' + oldValue + ' to value: ' + newValue + '.');
}
return false;
}
return false;
}
}
PicklistActivateService
@RestResource(urlMapping='/PicklistActivateService/*')
global without sharing class PicklistActivateService{
/**
* @routing /PicklistActivateService
* @description Activates a value on a Picklist. oldValue does not necessarily have to exist on the Picklist
* so this can also be used to create and immediately activate a value on a Picklist.
* @caution If the field's object uses Record Types, the Picklist Value needs to be added to applicable Record Types via PicklistValueRecordTypeService
* @param sObjectName String; API Name of the Object
* @param fieldName String; API Name of the field
* @param oldValue String; API Name of the old value
* @param newLabel String; it's possible to give the new value a different label; usually just set the same value as for oldValue
* @param position Integer; at which position the new value should be inserted
* @param isDefault Boolean; whether the activated value should be set as the Picklist's Default Value
* @param sortMe Boolean; with alpha sorting = true; without alpha sorting = false
* @return Boolean; success = true; failure = false
* Potential failure reasons: Object or field invalid; field not a picklist; values empty; Save errors
*/
@HttpPost
global static Boolean activatePicklistValue(String sObjectName, String fieldName, String oldValue, String newLabel, Integer position, Boolean isDefault, Boolean sortMe) {
// Creates Service and retrieves Picklist values
MetadataService.MetadataPort service = PicklistHelper.createService();
List<Schema.PicklistEntry> ple = PicklistHelper.retrievePicklistData(sObjectName, fieldName);
MetadataService.CustomField customField = PicklistHelper.createCustomField(sObjectName, fieldName);
// Create a new list of values
List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
// Loop through values retrieved from Global Value Set and let a counter run
// Fill the new list with retrieved values unless the retrieved value is the one to activate
// If the counter is at the desired position, put the new value here and set it to active
Integer i = 0;
for(Schema.PicklistEntry pickListVal : ple) {
i++;
if(i == position){
MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
newCustomValue.fullName = oldValue;
newCustomValue.label = newLabel;
newCustomValue.default_x = isDefault;
newCustomValue.isActive = true;
newValues.add(newCustomValue);
}
if(pickListVal.getValue() != oldValue){
MetadataService.CustomValue customValue = new MetadataService.CustomValue();
customValue.fullName = String.valueOf(pickListVal.getValue()); //API name of picklist value
customValue.label = String.valueOf(pickListVal.getLabel());
customValue.default_x = !isDefault ? pickListVal.isDefaultValue() : false;
customValue.isActive = pickListVal.getValue() == oldValue ? true : pickListVal.isActive();
newValues.add(customValue);
}
}
// Apply new list to Custom Field
customField = PicklistHelper.applyNewValueSet(customField, newValues, sortMe);
// Save changed Metadata
return PicklistHelper.saveMetadata(service, customField, true, 'Picklist value activated on Object: ' + sObjectName + ', field: ' + fieldName + ', value: ' + oldValue + '.');
}
}
PicklistAddToRecordTypeService
@RestResource(urlMapping='/PicklistAddToRecordTypeService/*')
global without sharing class PicklistAddToRecordTypeService{
/**
* @routing /PicklistAddToRecordTypeService
* @description Activates a Picklist Value for a Record Type.
*
* @param sObjectName String; API Name of the Object
* @param fieldName String; API Name of the field
* @param recordTypeDeveloperName String; API Name of the Record Type
* @param picklistValueToAdd String; name of the value to add.
* @param isDefault Boolean; whether the activated value should be set as the Picklist's Default Value
* @return Boolean; success = true; failure = false
* Potential failure reasons: Object or field invalid; field not a picklist; picklistValueToAdd not on the Picklist; values empty; Save errors
*/
@HttpPost
global static Boolean addPicklistValue(String sObjectName, String fieldName, String recordTypeDeveloperName, String picklistValueToAdd, Boolean isDefault) {
String auxSObjectName = (sObjectName == 'PersonAccount') ? 'Account' : sObjectName;
// Creates Service and retrieves Picklist values
MetadataService.MetadataPort service = PicklistHelper.createService();
// Generate field
MetadataService.CustomField customField = PicklistHelper.createCustomField(auxSObjectName, fieldName);
// Validate
List<Schema.PicklistEntry> picklistEntries = PicklistHelper.retrievePicklistData(auxSObjectName, fieldName);
// Check if picklistValueToAdd exists on the Picklist
Boolean hasValue = false;
for(Schema.PicklistEntry pickListVal : picklistEntries) {
if(String.valueOf(pickListVal.getValue()) == picklistValueToAdd){
hasValue = true;
break;
}
}
if(hasValue == false){
return hasValue;
}
// Retrieve Record Type and its Picklist Values
MetadataService.IReadResult readResult = service.readMetadata('RecordType', new String[] { sObjectName + '.' + recordTypeDeveloperName });
if(readResult != null){
MetadataService.RecordType recordType = (MetadataService.RecordType) readResult.getRecords()[0];
List<MetadataService.RecordTypePicklistValue> rtPickValues = recordType.picklistValues;
// Create new List of Record Type Picklist Values
List<MetadataService.RecordTypePicklistValue> newRTPicklistValues = new List<MetadataService.RecordTypePicklistValue>();
// Loop through all Picklists under the Record Type Picklist Values. When we hit the field, add the value.
for(MetadataService.RecordTypePicklistValue rtPickValue : rtPickValues){
MetadataService.RecordTypePicklistValue auxRTPickValue = new MetadataService.RecordTypePicklistValue();
auxRTPickValue.picklist = rtPickValue.picklist;
if(rtPickValue.picklist == fieldName){
List<MetadataService.PicklistValue> newValues = rtPickValue.values;
MetadataService.PickListValue newPickVal = new MetadataService.PickListValue();
newPickVal.fullName = picklistValueToAdd;
newPickVal.isActive = true;
newPickVal.default_x = isDefault;
newValues.add(newPickVal);
auxRTPickValue.values = newValues;
} else {
auxRTPickValue.values = rtPickValue.values;
}
newRTPicklistValues.add(auxRTPickValue);
}
// Apply new Record Type Picklist Values to Record Type
recordType.PicklistValues = newRTPicklistValues;
// Save changed Metadata
return PicklistHelper.saveMetadata(service, recordType, false, null);
}
return false;
}
}
PicklistDeactivateService
@RestResource(urlMapping='/PicklistDeactivateService/*')
global without sharing class PicklistDeactivateService{
/**
* @routing /PicklistDeactivateService
* @description Deactivates a value on a Picklist Field. If oldValue does not exist on the Picklist, no change is made.
* @param sObjectName String; API Name of the Object
* @param fieldName String; API Name of the field
* @param oldValue String; API Name of the old value
* @param sortMe Boolean; with alpha sorting = true; without alpha sorting = false
* @return Boolean; success = true; failure = false
* Potential failure reasons: Object or field invalid; field not a picklist; values empty; Save errors
*/
@HttpPost
global static Boolean deactivatePicklistValue(String sObjectName, String fieldName, String oldValue, Boolean sortMe) {
// Creates Service and retrieves Picklist values
MetadataService.MetadataPort service = PicklistHelper.createService();
List<Schema.PicklistEntry> ple = PicklistHelper.retrievePicklistData(sObjectName, fieldName);
MetadataService.CustomField customField = PicklistHelper.createCustomField(sObjectName, fieldName);
// Create a new list of values
List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
// Loop through values retrieved from Global Value Set and let a counter run
// If value is not the value to deactivate, put the value into the new list
for(Schema.PicklistEntry pickListVal : ple) {
MetadataService.CustomValue customValue = new MetadataService.CustomValue();
customValue.fullName = String.valueOf(pickListVal.getValue());
customValue.label = String.valueOf(pickListVal.getLabel());
customValue.default_x = pickListVal.isDefaultValue();
customValue.isActive = pickListVal.getValue() == oldValue ? false : pickListVal.isActive();
newValues.add(customValue);
}
// Apply new list to Custom Field
customField = PicklistHelper.applyNewValueSet(customField, newValues, sortMe);
// Save changed Metadata
return PicklistHelper.saveMetadata(service, customField, true, 'Picklist value deactivated on Object: ' + sObjectName + ', field: ' + fieldName + ', value: ' + oldValue + '.');
}
}