Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Field Tags Reference

A model’s behaviour is declared with three struct tags on each field. This page documents all three, and every directive accepted by the mfx tag.

The three tags

TagControlsDefault if omitted
jsonfield name in request and response bodiessnake_case of the Go field name
dbdatabase column namethe resolved json name
mfxfield behaviour — validation, querying, and moreno directives
Title string `json:"title" db:"title" mfx:"required,filterable,sortable"`

The mfx tag holds a comma-separated list of directives. Whitespace around each directive is trimmed. Directives are either flags (a bare word) or key-value directives (key:value). An unrecognised directive is ignored.

Excluding a field

A field is dropped from the model entirely — no column, not in any payload — if any of its three tags is set to -:

Internal string `mfx:"-"`     // excluded
Cache    string `json:"-"`    // excluded
Scratch  string `db:"-"`      // excluded

Validation directives

These constrain the values a field accepts on write.

DirectiveEffect
requiredthe field must be present in a create request
enum:a|b|cthe value must be one of the pipe-separated options
min:Nnumeric minimum (N is a number)
max:Nnumeric maximum
default:Vvalue applied when the field is absent; cast to the field’s type
Status   string `json:"status"   mfx:"required,enum:draft|published|archived"`
Priority int    `json:"priority" mfx:"min:1,max:5,default:3"`

Write-access directives

These govern whether a field can be set by a client, and when.

DirectiveEffect
readonlystripped from all write operations; values sent by a client are ignored
immutableaccepted on create, rejected on update

BaseModel’s created_at and updated_at are readonly. Use immutable for values that are set once and must not change afterwards, such as an owner ID.

Email   string `json:"email"    mfx:"required,immutable"`
ApiKey  string `json:"api_key"  mfx:"readonly"`

Response-visibility directives

Both directives drop the field from API responses. They differ in whether the client may write the field.

DirectiveRead in responsesWrite on create / update
writeonlynoyes
hiddennono
  • writeonly is for values the client must supply but should never see again — typically passwords. The field is included in the create and update request schemas; only the response is scrubbed.
  • hidden is for values clients have no business touching at all — server-managed internals, audit fields, derived data. The field is dropped from create and update schemas as well, so it cannot be set from the API. It is still stored, and code running inside the pipeline (a middleware on the Service step, for example) can populate it.
// Client sets it on create, never sees it back.
Password string `json:"password" mfx:"required,writeonly"`

// Server-managed; client can neither read nor write it.
InternalScore float64 `json:"internal_score" mfx:"hidden"`

A field that is both readonly and not hidden is the opposite case: visible in responses, never accepted from the client.

Record-locking directives

These freeze a whole record — not just a field — once its state matches a condition. Useful for terminal states in business workflows (posted invoices, closed pay periods, confirmed POs).

DirectiveEffect
lock_when:field=valuewhen the existing record’s field equals value, updates and deletes return 422 RECORD_LOCKED

Multiple lock_when directives accumulate; any matching condition locks the record. The directive can be written on any field — the referenced field is what matters. A typo in the referenced JSON name is caught at registration so you never ship a rule that silently never matches.

type Invoice struct {
    maniflex.BaseModel
    Number string
    Status string `mfx:"enum:draft|posted|void,lock_when:status=posted,lock_when:status=void"`
    Amount int
}

The transition into a locked state is itself allowed — when the request arrives, the loaded record is still in its previous state. After that update commits, the record becomes frozen.

lock_when is checked before the default Validate step’s other rules on update, and before the adapter’s Delete call on delete. Creates are exempt: there is no prior state to check.

Pessimistic lock directive

DirectiveEffect
lock_scope:ModelNamebefore a create, acquire a SELECT … FOR UPDATE lock on the row referenced by this field’s value

Eliminates manual ctx.LockForUpdate calls in the most common case: a create that must read-then-write a shared resource without a concurrent write sneaking in between.

type Dispense struct {
    maniflex.BaseModel
    StockID  string `json:"stock_id"  db:"stock_id"  mfx:"required,lock_scope:StockBalance"`
    Quantity int    `json:"quantity"  db:"quantity"  mfx:"required,min:1"`
}

Requirements:

  • The model must run inside a transaction. Register maniflex.WithTransaction(nil) on the Service step; otherwise the DB step aborts with 500 LOCK_SCOPE_NO_TX.
  • The referenced model name must be registered. A typo is caught at startup (in Handler()), so it never reaches production silently.
  • If the referenced row does not exist, the create returns 404 NOT_FOUND.
server.Pipeline.Service.Register(
    maniflex.WithTransaction(nil),
    maniflex.ForModel("Dispense"),
    maniflex.ForOperation(maniflex.OpCreate),
)

Comparison with ctx.LockForUpdate:

lock_scope tagctx.LockForUpdate
Declarationstruct tagcustom Service middleware
Fields lockedone per tag directiveany ID at runtime
Requires transactionyes (enforced at runtime)yes (enforced at call time)
Use whenone fixed FK to lockdynamic or multiple targets

See Transactions for the underlying ctx.LockForUpdate and BeginTx APIs.

Query directives

These opt a field into the query string. A field is not filterable or sortable unless explicitly tagged.

DirectiveEffect
filterablethe field may be used in ?filter=
sortablethe field may be used in ?sort=
searchablethe field is indexed for native full-text search (?q=); text columns only
cursor_field:<name>opt the model into keyset (cursor) pagination; <name> is the sortable column to walk by

See Querying for the filter, sort, and cursor-pagination grammar.

Schema directives

DirectiveEffect
uniquea hint to the adapter to add a UNIQUE constraint on the column
indexcreate a (non-unique) index on the column during AutoMigrate
Slug  string `json:"slug"  mfx:"required,unique"`
Email string `json:"email" mfx:"index"`

index creates an index named idx_<table>_<column>. It is skipped when the column is already covered by another index — a unique constraint on the same field (databases index unique columns implicitly), a ModelConfig.Indices entry, or a scheduled-column auto-index — so adding it is always safe. Indexing a foreign-key column (e.g. mfx:"index" on UserID) is a common, valid use.

Relation directives

A field may declare a relationship to another model. The legacy ID-suffix convention (a UserID field implies a relation to User) needs no tag; the directives below configure explicit relations.

DirectiveEffect
relation:Namemarks the field as an explicit relation; Name is the companion struct field carrying the target type
relation:Name;onDelete:actionsets the referential action — cascade, setNull, or restrict
through:Modelon a slice field, declares a many-to-many relation through the named junction model
norelationopt a convention-FK field (name ends in ID) out of the automatic relation; keep it a plain column

onDelete sub-options are joined to the relation: directive with a semicolon, not a comma. Relationships are covered in full in Relations.

By default a field whose name ends in ID (e.g. UserID) implies a BelongsTo relation to the matching model (User). Use norelation when the column is just an identifier — an external reference, an opaque token — that should not be resolved as a relation:

ExternalID string `json:"external_id" mfx:"norelation"` // stays a scalar column

File upload directives

file marks a field as a file-upload field. The column stores the storage key; multipart form-data is then accepted for create and update on the model.

DirectiveEffect
filemark the field as a file upload
max_size:Nmaximum file size; accepts KB, MB, GB suffixes, or plain bytes
accept:p1|p2allowed MIME-type patterns, e.g. image/*|application/pdf
auto_delete:falsekeep the stored file when the record is hard-deleted or the field is replaced (default: delete it)
file_acl:private(default) response carries the raw storage key
file_acl:signedresponse replaces the key with a pre-signed URL (TTL: Config.FileSignedURLTTL, default 1h)
file_acl:publicresponse replaces the key with a permanent / long-lived URL
Avatar string `json:"avatar" mfx:"file,max_size:2MB,accept:image/*"`
Logo   string `json:"logo"   mfx:"file,file_acl:public,accept:image/*"`

See File Fields & Uploads for the upload workflow.

Encryption directives

DirectiveEffect
encryptedthe field is encrypted at rest (AES-256-GCM) and decrypted on read
key:namethe key name passed to the key provider; defaults to default

Encrypted fields cannot be filtered or sorted, because the stored value is ciphertext. If unique is also set, a companion {field}_hmac column enforces uniqueness without exposing the plaintext.

SSN string `json:"ssn" mfx:"encrypted,key:patient-pii"`

Scheduled directives

The scheduled directive declares a time-driven transition on a timestamp field — for example, soft-deleting a row once a timestamp passes. The directive only marks the field; the transitions are applied by a background runner documented in Events & Background Jobs.

It is an advanced feature with several sub-options joined by semicolons:

ExpiresAt time.Time `json:"expires_at" mfx:"scheduled;soft-delete"`
PublishAt time.Time `json:"publish_at" mfx:"scheduled;field=status;from=draft;to=published"`
Sub-optionEffect
soft-deletesoft-delete the row when the timestamp is reached
hard-deletepermanently delete the row when the timestamp is reached
field=Fthe field to change
from=Vapply only when field currently equals V
to=Vthe value to set field to

Locale directives

These apply to fields declared as maniflex.LocaleString — a multilingual string stored as a JSON object keyed by locale code (e.g. {"en":"Finance","ar":"مالية"}).

Mark a field as locale-aware with mfx:"locale". All other locale directives require locale to be present.

DirectiveEffect
localemarks the field as a LocaleString; enables locale-aware response serialisation
split(default) response emits "name" = resolved string and "name_i18n" = full map
resolveresponse always emits "name" as a plain string; no companion field
dynamicresponse emits a string when ?locale= is set, the full map otherwise
default_locale:codefield-level fallback locale (e.g. default_locale:ar) when the client did not request a specific locale
type Department struct {
    maniflex.BaseModel
    Name maniflex.LocaleString `json:"name" mfx:"locale,filterable,sortable"`
    Bio  maniflex.LocaleString `json:"bio"  mfx:"locale,resolve,default_locale:ar"`
}

The resolved locale for a request follows a precedence chain: ?locale= param → Accept-Language header → default_locale tag → model DefaultLocale → app LocaleOptions.Default"en".

See Localization for the full LocaleResolver setup and filtering/sorting behaviour.

Quick reference

DirectiveCategory
requiredvalidation
enum:… min: max: default:validation
readonly immutablewrite access
hidden writeonlyresponse visibility
filterable sortable searchable cursor_field:…querying
unique indexschema
relation:… through:… norelationrelations
file max_size: accept: auto_delete:false file_acl:file upload
encrypted key:…encryption
scheduled;…scheduled transitions
locale split resolve dynamic default_locale:localisation
-exclude the field