@PalmyraField#

com.palmyralabs.palmyra.base.annotations.PalmyraField

Attaches metadata to an attribute — column mapping, sort/search flags, validation, ACL, and pre-save transforms. Target: FIELD. Retention: RUNTIME.

Attributes#

Attribute Signature
attribute String attribute() default "" — bean attribute name
value String value() default "" — alias for attribute
name String name() default "" — logical field name
column String column() default "" — DB column
title String title() default "" — display title
displayOrder int displayOrder() default 0
primaryKey boolean primaryKey() default false
keyField boolean keyField() default false — logical key field
primaryRef String primaryRef() default ""
parentRef String parentRef() default ""
search boolean search() default false — searchable
sort boolean sort() default false — sortable
quickSearch boolean quickSearch() default false
pattern String pattern() default "" — validation regex
notNull boolean notNull() default false
mandatory Mandatory mandatory() default Mandatory.NONE — operation-scoped required check (see Mandatory)
active boolean active() default true
virtual boolean virtual() default false — non-persistent
aclMask Acl aclMask() default Acl.DEFAULT
drop DropMode drop() default DropMode.NONE — strip the attribute in a given direction (see DropMode)
preProcessor Class<? extends Function<Object,Object>> preProcessor() default processor.class — pre-save transform
childMode ChildProcessMode childMode() default ChildProcessMode.MERGE

Example#

public class User {

    @PalmyraField(attribute = "id", column = "user_id", primaryKey = true)
    private Long id;

    @PalmyraField(attribute = "email", sort = true, search = true, quickSearch = true)
    private String loginName;

    @PalmyraField(attribute = "displayName", pattern = "^[A-Za-z ]{1,64}$", notNull = true)
    private String name;
}

Flattening with parentRef#

parentRef lets a DTO expose a scalar that’s actually sourced from a joined table. The client sees a flat JSON shape; the backend still does the join. The dotted path can live on either parentRef or attribute.

One hop — from an immediate parent#

// StockEntry → purchaseRef.purchaseDate
@PalmyraField(parentRef = "purchaseRef", attribute = "purchaseDate")
private LocalDate purchaseDate;

Two hops — dotted parentRef#

// StockEntry → purchaseRef → supplier → name
@PalmyraField(parentRef = "purchaseRef.supplier", attribute = "name")
private String supplier;

Two hops — dotted attribute#

Same result as the previous form — the dot is just on the other side:

// PurchasePaymentLineItem → purchase → supplier → name
@PalmyraField(parentRef = "purchase", attribute = "supplier.name")
private String vendor;

// PurchasePaymentLineItem → purchase → paymentStatus → code
@PalmyraField(parentRef = "purchase", attribute = "paymentStatus.code")
private String statusCode;

Real-world examples from the clinic sample live in StockEntryModel and PurchasePaymentLineItemModel. See the schema tutorial for the full walk-through.

Mandatory#

com.palmyralabs.palmyra.base.format.Mandatory

Controls when the attribute is required. The framework evaluates the rule against the current CRUD action — a field can be required on create but optional on update, or required on every write but ignored for deletes.

Constant Required on ACL rights mask
NONE never (default) 0
ALL every operation ACLRights.ALL
CREATE create (insert) ACLRights.READ_CREATE
UPDATE update ACLRights.READ_UPDATE
SAVE create or update through SaveHandler (upsert) ACLRights.READ_SAVE
DELETE delete ACLRights.READ | ACLRights.DELETE

The mandatory check runs inside the PreProcessor pipeline before the handler’s lifecycle hooks fire. A missing required value throws a validation error and the operation aborts — so your preCreate / preUpdate / preSave hooks never see a half-constructed tuple.

Example#

public class User {

    // Required on every operation — identity field
    @PalmyraField(attribute = "loginName", mandatory = Mandatory.ALL)
    private String loginName;

    // Required on create, optional on subsequent updates
    @PalmyraField(attribute = "password", mandatory = Mandatory.CREATE)
    private String password;

    // Required whenever the row is being written via SaveHandler (upsert)
    @PalmyraField(attribute = "tenantId", mandatory = Mandatory.SAVE)
    private Long tenantId;
}

Pair Mandatory with notNull for belt-and-braces validation — notNull is a schema-level guarantee; mandatory picks the action it applies to.

DropMode#

com.palmyralabs.palmyra.base.format.DropMode

Controls when the attribute is stripped from the payload as it crosses the serialization boundary. Use it to hide write-only or read-only columns without defining a separate DTO.

Constant Wire value Stripped on
NONE "none" Never — default
INCOMING "incoming" Requests — the client cannot write this attribute (silently ignored if supplied)
OUTGOING "outgoing" Responses — never serialized back to the client
NULL "ignore-null" Responses — skipped only when the value is null

Example#

public class User {

    // Server-managed — the client can read it but never set it
    @PalmyraField(attribute = "createdAt", drop = DropMode.INCOMING)
    private Instant createdAt;

    // Persisted + validated, but never echoed back in a response
    @PalmyraField(attribute = "passwordHash", drop = DropMode.OUTGOING)
    private String passwordHash;

    // Optional — keep the wire payload slim when it's absent
    @PalmyraField(attribute = "secondaryEmail", drop = DropMode.NULL)
    private String secondaryEmail;
}

INCOMING is the natural fit for audit columns (createdBy, updatedAt, tenantId) that your preCreate / preUpdate hook populates — clients can’t overwrite them, and the server stays authoritative. OUTGOING is how you protect sensitive columns (hashes, tokens, internal flags) without maintaining a shadow DTO.