BeforeUpdate.Validator
Rejects an update with an error message before it is saved. Use it to block a transition or to keep a value from being cleared.
Interface
apex
public class BeforeUpdate {
public interface Validator extends Handler {
Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record);
void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record);
}
}errorShouldBeAttachedOnBeforeUpdateWhen(record): called once per record in the chunk. Returntrueto reject the record.addErrorOnBeforeUpdate(record): called right after its predicate returns true; it must attach an error to the record, or the library throws.
Example
apex
public with sharing class ContactValidator implements BeforeUpdate.Validator {
public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChangedTo(Contact.Email, null);
}
public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
record.addError(Contact.Email, 'Email cannot be removed.');
}
}cls
public with sharing class OpportunityWinAmountValidator implements BeforeUpdate.Validator {
public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChangedTo(Opportunity.StageName, 'Closed Won') && (record.isNull(Opportunity.Amount) || record.lessThanOrEqualTo(Opportunity.Amount, 0));
}
public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
record.addError(Opportunity.Amount, 'Set a positive Amount before moving this Opportunity to Closed Won.');
}
}cls
public with sharing class AccountDemotionValidator implements BeforeUpdate.Validator {
public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
Account oldAccount = (Account) record.getOldSObject();
return record.isChangedTo(Account.Type, 'Prospect') && oldAccount.Type?.startsWith('Customer') == true;
}
public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
Account oldAccount = (Account) record.getOldSObject();
record.addError(Account.Type, 'This account is already ' + oldAccount.Type + ' and cannot be moved back to Prospect. Close it as Lost instead.');
}
}Good to Know
- Gate on a change. A Validator runs on every update, nested ones included, with no recursion limit. Without
isChanged, a record that fails the check cannot be updated at all, not even by automation. - Name and address fields lose the field. On them,
record.addError(field, message)shows the message at record level. To keep it on the field, call((Account) record.getNewSObject()).BillingCountry.addError(message). - Never call
addErrorongetOldSObject(). It throws aFinalExceptionthat nothing in the trigger can catch. - No DML. If the handler runs DML or publishes an event, the library throws.
- Populator wins. A class that also implements
BeforeUpdate.Populatorruns only as a Populator. Its Validator methods never run.
Test
apex
@IsTest
static void errorShouldBeAttachedOnBeforeUpdateWhenWonWithZeroAmount() {
// Setup
TriggerHandler.UpdateRecord record = new TriggerHandler.TriggerRecord(
new Opportunity(StageName = 'Closed Won', Amount = 0),
new Opportunity(StageName = 'Negotiation/Review')
);
// Test
Boolean result = new OpportunityWinAmountValidator().errorShouldBeAttachedOnBeforeUpdateWhen(record);
// Verify
Assert.isTrue(result, 'The record should be rejected.');
}