Mutation payloads
POST and PATCH bodies on the Public API follow different null semantics. The contract schemas describe which fields are required, optional, or nullable; this guide explains how omitted vs null values are interpreted at runtime.
Create (POST)
Create payloads are not partial. Every field the contract defines is resolved to a concrete value before persistence.
| Client sends | Meaning |
|---|---|
| Field omitted | Same as null for nullable fields — persist null (or the documented default when one exists). |
null |
Explicit null (or the documented default when one exists). |
| A value | Persist that value. |
Defaults apply when the field is omitted or null, for example:
POST /users—typedefaults toemployee.- Optional arrays on create — omitted or
nullmeans an empty list (no license types or roles attached).
Required fields (name, email on user create, path parameters, etc.) must be present with a non-null value.
Create body fields without a documented default persist as null when omitted or sent as null (for example billingStatus, grantedAt, grantedById on license create). The deserializer maps the body to the database shape; the use case does not fill in values the client did not send.
Example — create a user with no personal email:
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
personalEmail is omitted → stored as null, same as "personalEmail": null.
Update (PATCH)
Update payloads are always partial. Only fields present in the JSON body are considered; everything else is left unchanged.
| Client sent | Meaning |
|---|---|
| Field omitted | Do not update this field. |
null |
Set the field to null (only valid when the field is nullable in the contract). |
| A value | Set the field to that value. |
Non-nullable fields (for example name, email) can be updated to a new value but cannot be cleared with null.
Nullable fields (for example teamKey, billingStatus, grantedAt) must be declared .nullable().optional() on both create and update schemas when the resource DTO exposes them as nullable. Using .optional() alone on PATCH rejects null and prevents clearing — see Authoring mutation schemas below.
At runtime, omitted update fields are passed through to Prisma as undefined; Prisma omits those keys from the UPDATE statement, so unchanged columns are left untouched. Timestamps such as last_updated_at are set by the use case, not from the request body.
Array fields on update (for example licenseTypeKeys) follow a slightly different rule:
| Client sent | Meaning |
|---|---|
| Field omitted | Do not update the list. |
[] or a non-empty array |
Replace the entire list with the sent array (an empty array clears all items). |
Example — clear a user's team without touching other fields:
{
"teamKey": null
}
Example — change email only:
{
"email": "ada.lovelace@example.com"
}
teamKey, jobKey, and other omitted properties stay as they are.
Quick reference
CREATE omitted ≡ null → null (or default)
UPDATE omitted → skip field
UPDATE null → clear (nullable fields only)
Authoring mutation schemas (implementers)
When adding fields to packages/public-api-dtos/src/<model>/mutations.ts:
- Start from the resource DTO — if
publicApi*DtoSchemamarks the field.nullable(), create and update body schemas must use.nullable().optional()(update always needs.optional()as well for patch semantics). - Do not confuse optional with nullable —
.optional()onPATCHmeans “client may omit the key”;.nullable()means “client may sendnullto clear”. Partial updates need both for nullable columns. - Arrays on update — use
.optional()only; clearing is[], notnull(create may still use.nullable().optional()wherenullmeans empty list). - Required columns — create: required or
.default(); update:.optional()without.nullable().
Agent context: mutation-deserializers.md § Mutation body schemas.
See the API reference for per-field nullability and the Models section for full request body schemas.