diff --git a/src/modules/creator/creator-profile.schemas.ts b/src/modules/creator/creator-profile.schemas.ts index 6e5cb0f..423f57f 100644 --- a/src/modules/creator/creator-profile.schemas.ts +++ b/src/modules/creator/creator-profile.schemas.ts @@ -32,6 +32,9 @@ export const CreatorPerkSchema = z.object({ * * The shape is explicit now so future indexing-backed values can be dropped in * without changing API contracts. + * + * @see ../creators/creators.serializers.ts — module docblock for null vs absent + * field rules across creator list and detail responses. */ export const CreatorProfileReadResponseSchema = z.object({ creatorId: z.string(), diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index 4bd3d19..606799c 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -1,3 +1,53 @@ +/** + * ## Optional fields in public creator responses (null vs absent) + * + * Success bodies are sent with Express `res.json()`. In JSON output: + * + * - **`null`** — the key is present and the value is JSON `null`. + * - **Absent** — the key is omitted when the JavaScript value is `undefined` + * (standard `JSON.stringify` behavior). + * + * Clients should treat **`null` and absent differently** only where documented below; + * elsewhere, assume the key is always present when listed. + * + * ### Creator list (`GET /api/v1/creators`) + * + * Items use {@link CreatorListItem} from `creator-list-item.mapper.ts` + * (`serializeCreatorList` → `mapCreatorListItem`). + * + * | Field | When empty / unknown | + * |-------|----------------------| + * | `id`, `followers` | Always present (`followers` is a number, currently `0`). | + * | `name`, `avatar` | **Always present**; use JSON **`null`** when display name or avatar URL is missing (`?? null` in the mapper). | + * + * List envelope (`items`, `meta`) always includes both keys. Offset `meta` always + * includes `limit`, `offset`, `total`, and `hasMore`. + * + * **Deviations** + * + * - **Cursor list `meta.nextCursor`**: JSON **`null`** when there is no next page + * (`serializeCreatorListCursorMeta`), not omitted. + * - **`CreatorSummary` / `serializeCreatorSummary`**: not used by the active list + * route today; if reused, `avatarUrl` would be **omitted** when `undefined` + * (optional property), unlike `CreatorListItem.avatar` which uses **`null`**. + * + * ### Creator detail (`GET /api/v1/creators/:creatorId/profile`) + * + * Shape: `CreatorProfileReadResponse` in `creator-profile.schemas.ts`, + * built by `getCreatorProfile` in `creator-profile.service.ts`. + * + * | Field | When empty / unknown | + * |-------|----------------------| + * | `creatorId`, `metadata` | Always present. | + * | `displayName`, `bio`, `avatarUrl` | **Always present**; JSON **`null`** when unset (Zod `.nullable()`; placeholder fallback uses explicit `null`). | + * | `links` | **Always present** as an array; use **`[]`** when there are no links (never `null`, never omitted). | + * | `perks` | **Always present** as an array in current handlers (`[]` when none). The read schema marks `perks` optional, but the service always includes the key. | + * + * When adding new optional creator response fields, pick one strategy deliberately: + * nullable keys for “known empty”, omitted keys only when the field is truly + * inapplicable, and empty arrays for “none yet” collections. + */ + import { CreatorProfile } from '../../types/profile.types'; import type { CursorPaginationMeta } from '../../types/cursor.types'; import type { OffsetPaginationMeta } from '../../utils/pagination.utils';