Every field-level validation attribute in Acumatica boils down to a handler wired onto RowPersisting or FieldVerifying on the DAC's cache. PXDefault, PXUIField, and the family of PXRule-style validation attributes are just pre-packaged versions of that pattern, and once you see them as handlers-in-disguise, writing your own stops being mysterious.
What an attribute actually attaches to
A DAC field attribute is a class deriving from PXEventSubscriberAttribute (directly, or through a base like PXAggregateAttribute). When the cache for that DAC is created, the attribute's constructor registers delegate handlers for the events it cares about — most commonly FieldVerifying, FieldUpdated, and FieldDefaulting. This means a validation attribute and a hand-written RowPersisting handler in a graph extension are solving the same problem from two different layers: the attribute lives with the DAC and applies everywhere that field is used, the graph-level handler is scoped to one screen.
Building a custom range-validation attribute
A common real-world case: a percentage field that must sit between 0 and 100, with a message that names the actual value that failed. Acumatica's stock PXRange checks are close but don't format the error the way business users expect, so teams often roll a small custom attribute:
public class PercentRangeAttribute : PXEventSubscriberAttribute, IPXFieldVerifyingSubscriber
{
public virtual void FieldVerifying(PXCache sender, PXFieldVerifyingEventArgs e)
{
if (e.NewValue == null) return;
decimal value = (decimal)e.NewValue;
if (value < 0m || value > 100m)
{
e.NewValue = e.OldValue;
e.Cancel = true;
throw new PXSetPropertyException(
"Value {0} is outside the allowed 0-100 percent range.",
PXErrorLevel.Error, value);
}
}
}
Implementing IPXFieldVerifyingSubscriber directly rather than overriding a base attribute keeps the class small and makes the intent obvious to the next developer who opens it. The typical pattern is to throw PXSetPropertyException rather than setting an error on the cache directly — it integrates with the standard error-highlighting on the field and participates correctly with the persist pipeline.
FieldVerifying vs RowPersisting for validation
FieldVerifying fires as the value leaves the UI control and enters the cache, before the row is even necessarily complete — it's the right place for "is this value structurally valid" checks that don't depend on other fields. RowPersisting fires once, right before the row is written to the database, and is the right place for validations that need the full row state, cross-reference other rows, or must only run on save rather than on every keystroke-equivalent field change. A validation attribute that needs to compare two sibling fields (ship date must not precede order date) is often better expressed as a RowPersisting handler in a graph extension than as a per-field attribute, precisely because the attribute only sees the one field it's attached to.
You can also raise a non-blocking warning by calling PXUIField.SetError or sender.RaiseExceptionHandling directly on the cache instead of throwing. Throwing from FieldVerifying stops the value from being committed to the cache at all; setting a warning through the UI field lets the value through but flags it — useful for "this looks unusual, please confirm" rather than "this is invalid."
Stacking attributes and ordering
DAC fields commonly carry several attributes at once — PXDBDecimal, PXUIField, PXDefault, and a custom validation attribute all on the same property. Each attribute that subscribes to the same event (say, FieldVerifying) gets called in roughly the order the attributes are declared, and any one of them can cancel the operation. This matters when a custom validation attribute depends on formatting or default-value logic that another attribute is responsible for — put the custom attribute after the ones it depends on, and test what happens when a customization project layers yet another attribute onto the same field via a DAC extension, since the ordering across extensions is less predictable than ordering within one class.
Testing validation attributes in isolation
Because a validation attribute is tied to the cache event pipeline rather than to a specific screen, it's realistic to unit-test it by constructing a bare PXCache for the DAC (or a minimal graph) and asserting that setting an out-of-range value raises the expected exception. This is worth doing for anything beyond a trivial range check — validation logic buried inside a screen-specific event handler is much harder to exercise without spinning up the full graph and UI context.
Wrapping up
Custom validation attributes are the right tool when a rule belongs to the field wherever it appears, not to one screen. When the rule genuinely only makes sense on one screen, or needs to see fields outside the one it's attached to, a graph extension's RowPersisting or FieldVerifying override is usually the cleaner fit — and the two approaches compose fine on the same DAC.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.