BeforeInsert.Validator
Rejects new records before they are saved, with an error on the record or on a field. It works like a validation rule written in Apex.
Interface
apex
public class BeforeInsert {
public interface Validator extends Handler {
Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record);
void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record);
}
}errorShouldBeAttachedOnBeforeInsertWhen(record): called once per record in the chunk. Returntrueto reject the record.addErrorOnBeforeInsert(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 BeforeInsert.Validator {
public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
return record.isBlank(Contact.Email);
}
public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
record.addError(Contact.Email, 'Email is required.');
}
}cls
public with sharing class OpportunityAmountValidator implements BeforeInsert.Validator {
public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
return record.isNotNull(Opportunity.Amount) && record.lessThanOrEqualTo(Opportunity.Amount, 0);
}
public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
record.addError(Opportunity.Amount, 'Amount must be greater than zero. Leave it empty until the deal value is known.');
}
}cls
public with sharing class ContactEmailFormatValidator implements BeforeInsert.Validator {
public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
if (record.isBlank(Contact.Email)) {
return false;
}
return record.doesNotContain(Contact.Email, '@') || this.isSharedMailbox(record);
}
public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
if (record.doesNotContain(Contact.Email, '@')) {
record.addError(Contact.Email, 'Email must be a full address, for example jane.doe@example.com.');
return;
}
record.addError(Contact.Email, 'A shared mailbox cannot be used as a contact email. Use the person\'s own address.');
}
private Boolean isSharedMailbox(TriggerHandler.InsertRecord record) {
for (String prefix : new List<String>{ 'info@', 'sales@', 'support@', 'contact@', 'admin@', 'noreply@', 'no-reply@' }) {
if (record.startsWith(Contact.Email, prefix)) {
return true;
}
}
return false;
}
}Good to Know
- Every branch must add an error. If the error method returns without one, the library throws and the whole chunk fails, even with ContinueOnError.
- Errors add up.
addErrordoes not stop the run. Later handlers still run for the record, and each Validator that rejects it adds its own message. - Name and address fields lose the field. On
Nameor a compound address field such asBillingCity,record.addError(field, message)shows the message at record level. Use((Account) record.getNewSObject()).Name.addError(message)instead. - Populator wins. A class that also implements
BeforeInsert.Populatorruns only as a Populator. List Validators after the Populators, so they check the final values. - No DML. If the handler runs DML or publishes an event, the library throws.
Test
apex
@IsTest
static void addErrorOnBeforeInsertAttachesErrorToAmount() {
// Setup
Opportunity newOpportunity = new Opportunity(Amount = -100);
// Test
new OpportunityAmountValidator().addErrorOnBeforeInsert(new TriggerHandler.TriggerRecord(newOpportunity, null));
// Verify
Assert.areEqual(new List<String>{ 'Amount' }, newOpportunity.getErrors()[0].getFields(), 'The error should be attached to Amount.');
}