Skip to content

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. Return true to 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. addError does 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 Name or a compound address field such as BillingCity, 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.Populator runs 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.');
}