INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST: <value>: bad value for restricted picklist field
You wrote a value to a Restricted Picklist that isn't in the picklist's value list. Restricted means exactly that — only configured values are accepted, no free-form. The fix is to add the value to the picklist or clean the input.
Also seen asINVALID_OR_NULL_FOR_RESTRICTED_PICKLIST·bad value for restricted picklist·Invalid picklist value·restricted picklist value not in list
A data import dumps a CSV of account industries from an external system. The import dies on the seventh row: INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST: Manufacturing - Industrial: bad value for restricted picklist field: Industry. The seventh row's Industry is "Manufacturing - Industrial", which exists in the source system but not in Salesforce's Industry picklist. The external system was the source of truth; Salesforce wasn't aware of the new value.
What the platform is enforcing
A Restricted Picklist is a Salesforce field type that accepts only values from a configured list. Salesforce checks every write against that list. Values not in the list are rejected with INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST. The check fires whether the write comes from the UI, the API, Apex DML, or any other path.
Two flavors of picklist:
- Restricted Picklist: values are limited to the configured set. Writes outside the set fail.
- Non-restricted Picklist (sometimes called "unrestricted"): writes outside the set are allowed but flagged as inactive values that don't appear in dropdowns.
The restricted variant is more common for fields where data quality matters (Industry, Status, Stage, Type). Non-restricted is sometimes used for backward-compatibility scenarios.
The broken example
A bulk import that pushes external Industry values into Salesforce:
public static void importAccounts(List<Map<String, String>> rows) {
List<Account> accounts = new List<Account>();
for (Map<String, String> row : rows) {
accounts.add(new Account(
Name = row.get('name'),
Industry = row.get('industry'),
BillingCountry = row.get('country')
));
}
insert accounts;
}
If the external industry value doesn't match a Salesforce Industry picklist value, the insert fails. The whole batch rolls back unless partial-success is used.
The fix: validate against the picklist's allowed values
Read the allowed values from the picklist definition and validate input before save:
public static void importAccounts(List<Map<String, String>> rows) {
Set<String> allowedIndustries = getActivePicklistValues(Account.Industry);
List<Account> accounts = new List<Account>();
List<String> errors = new List<String>();
for (Integer i = 0; i < rows.size(); i++) {
Map<String, String> row = rows[i];
String industry = row.get('industry');
if (industry != null && !allowedIndustries.contains(industry)) {
errors.add('Row ' + i + ': industry "' + industry + '" is not a valid picklist value.');
industry = null; // Or apply a default
}
accounts.add(new Account(
Name = row.get('name'),
Industry = industry,
BillingCountry = row.get('country')
));
}
if (!errors.isEmpty()) System.debug(LoggingLevel.WARN, errors);
if (!accounts.isEmpty()) Database.insert(accounts, false);
}
private static Set<String> getActivePicklistValues(Schema.SObjectField field) {
Set<String> values = new Set<String>();
for (Schema.PicklistEntry entry : field.getDescribe().getPicklistValues()) {
if (entry.isActive()) values.add(entry.getValue());
}
return values;
}
The validation lets you decide what to do with invalid values: log and skip the field, log and skip the row, or log and substitute a default.
The fixed example, with default substitution
A complete service that maps unknown values to a sensible default:
public class AccountImportService {
private static final String INDUSTRY_DEFAULT = 'Other';
public class Result {
@AuraEnabled public Integer rowIndex;
@AuraEnabled public Boolean success;
@AuraEnabled public String mappedIndustry;
@AuraEnabled public String warning;
@AuraEnabled public String error;
}
public static List<Result> importAccounts(List<Map<String, String>> rows) {
Set<String> allowedIndustries = getActivePicklistValues(Account.Industry);
List<Account> toInsert = new List<Account>();
List<Result> results = new List<Result>();
for (Integer i = 0; i < rows.size(); i++) {
Map<String, String> row = rows[i];
Result r = new Result();
r.rowIndex = i;
String rawIndustry = row.get('industry');
String finalIndustry = rawIndustry;
if (rawIndustry != null && !allowedIndustries.contains(rawIndustry)) {
if (allowedIndustries.contains(INDUSTRY_DEFAULT)) {
finalIndustry = INDUSTRY_DEFAULT;
r.warning = 'Unknown industry "' + rawIndustry + '" mapped to "' + INDUSTRY_DEFAULT + '"';
} else {
finalIndustry = null;
r.warning = 'Unknown industry "' + rawIndustry + '" cleared';
}
}
r.mappedIndustry = finalIndustry;
toInsert.add(new Account(Name = row.get('name'), Industry = finalIndustry));
results.add(r);
}
Database.SaveResult[] saveResults = Database.insert(toInsert, false);
for (Integer i = 0; i < saveResults.size(); i++) {
if (saveResults[i].isSuccess()) {
results[i].success = true;
} else {
results[i].success = false;
results[i].error = saveResults[i].getErrors()[0].getMessage();
}
}
return results;
}
}
The service maps unknown industries to a default and records the mapping. The import succeeds; the data quality issue is logged for follow-up.
When the picklist needs new values
If the external system's values are legitimate and should be supported, the right fix is to add them to the picklist:
- Setup → Object Manager → Account → Fields → Industry.
- Edit the picklist values.
- Add the new values.
- Save.
Adding a value to a managed-package picklist isn't possible from the consuming org; the package author must add it. If you depend on a managed-package picklist, raise a feature request to the vendor.
For very large value sets (thousands of options), Salesforce supports Global Value Sets that can be shared across multiple picklist fields. A single value set update propagates to every consuming field.
Picklist value vs picklist label
A subtle distinction: picklist entries have a value (the API name) and a label (the human-readable display). Writes must use the value, not the label.
// Wrong: writing the label
account.Industry = 'Technology Services'; // Label
// Right: writing the value
account.Industry = 'Tech_Services'; // Value (sometimes the same as the label)
For most picklists, the value and label are identical (Salesforce auto-derives the value from the label). For some, they differ. When importing from external systems, always validate against getValue(), not getLabel().
Inactive picklist values
A picklist can have inactive values: values that used to exist, still apply to old records, but shouldn't be used for new records. The Schema describe API distinguishes:
for (Schema.PicklistEntry entry : Account.Industry.getDescribe().getPicklistValues()) {
System.debug(entry.getValue() + ' active: ' + entry.isActive());
}
For new-record writes, validate against isActive() == true. Old records with inactive values can stay; you just can't write new records with those values.
Multi-select picklists
Multi-select picklist fields accept multiple values, encoded as a semicolon-separated string:
account.MultiPicklist__c = 'Value1;Value2;Value3';
Each individual value must be a valid picklist entry. If any one is invalid, the entire write fails with INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST. Validation logic should check each value separately:
List<String> values = inputValue.split(';');
for (String v : values) {
if (!allowedValues.contains(v.trim())) {
// Reject or substitute
}
}
Record-type-specific picklists
Picklist values can be scoped to a specific Record Type: Account record type "Customer" might allow Industries A, B, C while record type "Partner" allows A, B, D. Writing a value that's valid for one record type but not the other (relative to the record's actual type) fails the same way.
Always validate picklist values against the record's record-type-specific allowed set:
Schema.RecordTypeInfo rtInfo = Account.Industry.getDescribe().getPicklistValues();
// Note: record-type-specific picklist values require the UIRecordTypeId API.
Record-type-aware validation is more complex; use the Lightning Data Service getPicklistValuesByRecordType wire adapter in LWC to get the right values for a given record type.
Dependent picklists
A dependent picklist's allowed values are determined by another picklist's selected value. The Industry picklist might depend on Country: a Country of "US" allows Industries A, B, C, while "UK" allows A, D, E.
INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST fires if the dependent picklist's value isn't in the set allowed by the controlling picklist. The fix requires understanding the dependency chain: set the controlling value first, then the dependent value.
Bulk-import data quality patterns
For data imports that hit picklist issues regularly, build a translation layer:
private static final Map<String, String> INDUSTRY_TRANSLATIONS = new Map<String, String>{
'Manufacturing - Industrial' => 'Manufacturing',
'Tech / IT Services' => 'Technology',
'Financial Svc' => 'Financial Services'
};
The translation map converts known external values to Salesforce picklist values. Unknown values fall through to the default-substitution path. The map lives in code (or, for admin-editable mappings, in a Custom Metadata Type).
Reporting on picklist drift
A weekly audit catches data-quality drift early: count records with each picklist value and look for unusual entries. If a value appears once in 200,000 records, it might be a one-off import error.
SELECT Industry, COUNT(Id)
FROM Account
GROUP BY Industry
ORDER BY COUNT(Id) DESC
Run this monthly. Look for orphan values (values that appear on records but aren't in the picklist's active list). They indicate either a non-restricted picklist or historical data with values that have since been deactivated.
When the picklist is on a managed-package object
If your import is to a managed-package object (an installed app's custom object), the picklist values are owned by the package vendor. You can't edit them. Three options:
- Use only values the vendor's package supports. Validate against the live picklist before import.
- Request the vendor add additional values via their support channel.
- Map external values to the vendor's supported set in your translation layer.
For active integrations with managed packages, the third pattern is usually the most maintainable. The translation map evolves as the vendor's picklist evolves.
Picklists and Process Builder / Flow
Validation rules and automation can reference picklist values. If you deactivate a value that an automation still references, the automation breaks silently. Audit automation references before deactivating values:
- Setup → Process Builder, Flow Builder, Workflow Rules: search for the value's name in formulas.
- Setup → Validation Rules: search for the value's name in formulas.
- Apex source: grep for the value string.
Deactivating a referenced value can produce save failures that look like INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST but are actually validation-rule failures on the automation's side. The error message is the same; the cause is different.
How the platform handles non-restricted picklists
For non-restricted picklists, writing an unknown value succeeds. The value gets stored as-is. The next time you view the record, the picklist dropdown shows the unknown value with a note that it's not in the active list. Reports can filter by the value normally.
This is more permissive but produces data quality drift. Most teams convert non-restricted picklists to restricted over time, with a one-time data cleanup to remove orphan values.
Reading picklists via LWC
In Lightning Web Components, getPicklistValues from lightning/uiObjectInfoApi fetches the current allowed values:
import { LightningElement, wire } from 'lwc';
import { getPicklistValues } from 'lightning/uiObjectInfoApi';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
export default class IndustryPicker extends LightningElement {
@wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT })
accountInfo;
@wire(getPicklistValues, {
recordTypeId: '$accountInfo.data.defaultRecordTypeId',
fieldApiName: INDUSTRY_FIELD
})
picklistValues;
get options() {
return this.picklistValues?.data?.values?.map(v => ({ label: v.label, value: v.value })) ?? [];
}
}
The wire returns the active values for the field, respecting record type if applicable. Use this pattern for any LWC that needs to render a picklist.
A diagnostic for picklist-restricted issues
When a save fails with INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST:
- Note the exact value the platform rejected.
- Open Setup → Object Manager → object → Fields → field.
- Click "Values" and review the active list.
- Confirm whether the rejected value should be added or whether the source data is wrong.
- Apply the appropriate fix on the side that needs to change.
The triage takes a few minutes. The fix depends on whether the picklist or the source is canonical for that value.
Empty string vs null
A subtle case: the platform treats an empty string as a different value from null for picklist fields. Writing '' (empty string) to a restricted picklist that doesn't include an empty value can fire INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST even though the field appears blank.
The fix: write null explicitly when you want the field empty, not an empty string:
account.Industry = String.isBlank(rawValue) ? null : rawValue;
The String.isBlank check catches both null and empty cases and normalizes to null.
Apex tests for restricted picklists
A test that verifies the import handles bad picklist values:
@isTest
static void importAccounts_substitutesUnknownIndustry() {
List<Map<String, String>> rows = new List<Map<String, String>>{
new Map<String, String>{ 'name' => 'Test', 'industry' => 'NotAValidIndustry' }
};
Test.startTest();
List<AccountImportService.Result> results = AccountImportService.importAccounts(rows);
Test.stopTest();
System.assertEquals(1, results.size());
System.assert(results[0].warning != null, 'Should warn about unknown industry');
}
The test confirms graceful handling rather than save failure. It works in any org because it uses a deliberately invalid value, regardless of which industries are configured.
A real-world refactor
A team's integration had been silently failing on 5% of records for years because the source system's industry values drifted from Salesforce's. The fix:
- Built a translation table in Custom Metadata mapping external values to Salesforce values.
- Added the validation layer to the import service.
- Built a weekly admin report on unmapped values so the team could update the translation table proactively.
- Set the default substitution to "Other" so new unknown values didn't break imports.
After the refactor: 0 failed rows, 12 new translations added in the first quarter, gradual convergence of source and target picklist values over time.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Validation errors
DUPLICATE_VALUE: duplicate value found
ValidationYou tried to insert or update a record with a value on a unique field (External ID set to "Unique" or one of the unique standard fields like…
ENTITY_IS_DELETED: entity is deleted
ValidationYou tried to update or query a record that's in the recycle bin (soft-deleted). Either undelete it first, query with `ALL ROWS` to include d…
ENTITY_IS_LOCKED: entity is locked for approval
ValidationThe record is currently in an Approval Process and is locked for editing until the approval is granted, rejected, or recalled. The lock is e…
FIELD_CUSTOM_VALIDATION_EXCEPTION: <your validation rule's error message>
ValidationA validation rule on the object evaluated to TRUE for the record being saved, blocking the save. The text after the colon is the rule's own …
INACTIVE_OWNER_OR_USER: operation performed with inactive User
ValidationYou set a record's Owner (or another User reference) to a deactivated user. Salesforce blocks new ownership pointing at inactive users by de…