Field-schema decorators
Pick the correct field-schema decorator for the handler you just scaffolded, wire it on main(), and let it handle validation, sanitation, and output masking — instead of duplicating those checks by hand.
Before you start
Section titled “Before you start”- You have a backend module scaffolded and its
*.field-schema.tsfile exists on disk. The CLI generated it from the module’s YAMLaggregateProperties; do not hand-edit it. - You know which role the handler plays: does it receive a payload? does it return the entity?
@aurorajs.dev/core-backand@aurorajs.dev/core-commonare installed — both ship with every Catalyst backend.
-
Pick the decorator using this decision tree.
Does the handler RECEIVE a payload?├── No → @Format(schema)└── Yes → Does it RETURN the entity?├── Yes → @ApplySchema(schema)└── No → @Sanitize(schema)@ApplySchemacomposes sanitize (input) + format (output).@Formatonly formats the return (reads are idempotent).@Sanitizeonly sanitizes the payload (write-only flows whose return isvoid, a boolean, or a summary type). -
Wire
@ApplySchemaon a write handler that returns the entity. Real example fromiam/tag:import { ApplySchema, EmitEvent } from '@aurorajs.dev/core-back';import { IamTagFieldSchema } from '@app/iam/tag/domain';@EmitEvent('iam.tag.created')@ApplySchema(IamTagFieldSchema)async main(payload: IamCreateTagInput, handlerMeta?: HandlerMeta): Promise<IamTag> {await this.createService.main(payload, handlerMeta);return await this.findByIdService.main(payload.id, {}, handlerMeta);}sanitizeruns onpayloadBEFOREmain()— oniam/user, for example, that is wheretype: 'password'turns plaintext into a bcrypt hash.formatruns on the return AFTERmain()— that is wherepasswordbecomesundefinedon the way out. -
Wire
@Formaton a read handler.import { Format } from '@aurorajs.dev/core-back';@Format(IamTagFieldSchema)async main(id: string, constraint?: QueryStatement, handlerMeta?: HandlerMeta): Promise<IamTag> {const tag = await this.findByIdService.main(id, constraint, handlerMeta);if (!tag) throw new NotFoundException(`IamTag with id: ${id}, not found`);return tag;}@Formatdetects the return shape automatically — single object, array, orPaginationwith a.rowslist — and formats each element. -
If the payload is not the first argument, use the object form.
@ApplySchema({ schema: IamTagFieldSchema, payloadIndex: 1 })async main(constraint, payload, handlerMeta?) { … }Same option on
@Sanitize.@Formathas nopayloadIndex— it always operates on the return value. -
On update handlers, compute the delta with
Obj.diff. After sanitation, persist only what changed. Real usage fromiam-update-tag-by-id.handler.ts:import { Obj } from '@aurorajs.dev/core-common';const tag = await this.findByIdService.main(payload.id, constraint, handlerMeta);if (!tag) throw new NotFoundException(`IamTag with id: ${payload.id}, not found`);const dataToUpdate = Obj.diff(payload, tag);await this.updateByIdService.main({ ...dataToUpdate, id: payload.id }, // re-add id — diff omits matching keysconstraint,handlerMeta,);Obj.diffomits keys whose values match between both sides, so theidmust be re-added explicitly as the target key.
Verify it worked
Section titled “Verify it worked”- For a
type: 'password'field, create a user and inspect the row: the column stores a bcrypt hash, never plaintext. Read it back — thepasswordkey is absent from the response payload. - For
maxLength,enumOptions, ornullable, send a payload that violates the constraint. The decorator throws beforemain()executes. - Timestamps returned by the handler carry the caller’s timezone. That confirms
@Format/@ApplySchemaextractedhandlerMeta.timezone— a direct call toformatRecord()would not.
Troubleshooting
Section titled “Troubleshooting”An included relation leaks a sensitive field. formatRecord does not recurse into relations loaded via include. That is a separate concern — see Compose a QueryStatement.
Validation runs twice — once by the decorator, once inside main(). Remove the in-handler check. @ApplySchema already enforced maxLength, nullable, enumOptions, and declared rules. Keep only business rules the FieldSchema cannot express (cross-field invariants, domain math).
You reach for a Value Object to encapsulate validation. Catalyst has no VO layer — the validation and modification concern is fully delegated to FieldSchema + type handlers via @ApplySchema. Express the rule in the YAML’s aggregateProperties and regenerate.
Direct call to formatRecord() or sanitizeRecord(). Avoid it. The decorator wires handlerMeta.timezone and other options automatically; a direct call loses that context and produces inconsistent output.
Related
Section titled “Related”- Backend module scaffolding — why
*.field-schema.tsis generated from YAML and never hand-edited. - Compose a QueryStatement — the query side of the declarative data-access layer.