Prisma JSON Casts — почему они допустимы
Проблема
В кодовой базе ~55 кастов вида:
// JSON READ: Prisma.JsonValue → typed
const rewards = season.rewards as unknown as SeasonRewards;
// JSON WRITE: typed → Prisma.InputJsonValue
data: { rewards: preparedRewards as unknown as Prisma.InputJsonValue }
Почему эти касты НЕ являются code debt
Корневая причина
Prisma не поддерживает typed JSON columns. JSON-колонки всегда типизированы как JsonValue (чтение) и InputJsonValue (запись). Это известное ограничение Prisma.
Почему Zod validation при чтении — YAGNI
Все JSON-колонки в проекте записываются нашим же кодом через typed interfaces:
| JSON-колонка | Кто пишет | Источник данных |
|---|---|---|
Season.rewards | admin-season-rewards.controller.ts | Validated admin body |
Season.referralRewards | admin-season-rewards.controller.ts | Validated admin body |
Season.boostLeaderboardRewards | admin-season-rewards.controller.ts | Validated admin body |
PromoCode.rewards | promo-code.repository.ts | CreatePromoCodeData (typed) |
Quiz.options | quiz.service.ts | validateOptions() result |
Quiz.correctAnswer | quiz.service.ts | Validated input |
SteamTrade.items* | Webhook handler | Sanitized webhook payload |
CraftHistory.materialsUsed | craft.service.ts | Computed in transaction |
SpinResult.rewardSnapshot | user-spin.service.ts | Constructed in code |
CaseOpening.rewardSnapshot | case-opening.service.ts | Constructed in code |
UserSettings.dismissedHints | settings.service.ts | Simple key-value map |
ConsolationAward.rewards | Admin/system | Typed config |
Данные не могут "сломаться" при чтении, потому что:
- Мы сами их записали через typed код
- Никто не пишет напрямую в БД мимо приложения
- Миграции не меняют JSON-структуру (только Prisma schema, не содержимое)
Когда Zod validation НУЖЕН
Zod/runtime validation оправдан только если:
- JSON пишется внешней системой (не нашим кодом)
- Есть миграция, меняющая структуру JSON (старые записи vs новые)
- JSON доступен для прямого редактирования пользователем
В нашем проекте ни одно из этих условий не выполняется.
Категории допустимых кастов
| Категория | Кол-во | Причина | Фиксить? |
|---|---|---|---|
JSON WRITE (→ Prisma.InputJsonValue) | ~24 | Ограничение Prisma | Нет |
JSON READ (JsonValue → Type) | ~28 | Ограничение Prisma | Нет |
Multipart (part as unknown as { value }) | 3 | Ограничение @fastify/multipart | Нет |
UTM validator (Record<string, unknown>) | 1 | TypeScript indexed writes | Косметика |
Что было сделано (Plans 00-04)
Plans 00-04 убрали все реальные type safety проблемы:
| План | Что убрал | Кастов |
|---|---|---|
| Plan 00 | (fastify as any).prisma → typed decorators | ~43 |
| Plan 01-02 | request.body as Type → route generics | ~110 |
| Plan 03 | Mapper/include type mismatches → as const satisfies + GetPayload | 5 |
| Plan 04 | Service return type mismatches → explicit return types | 8 |
| Итого | ~166 |
Оставшиеся ~56 кастов — граница типизации ORM, не код-долг.
Правило для будущих рефакторингов
НЕ создавать readJsonField<T>() / writeJsonField<T>() хелперы.
Они перемещают каст внутрь функции, но не добавляют type safety:
// Внутри readJsonField — тот же каст:
return value as unknown as T; // TypeScript по-прежнему "верит на слово"
Это code organization (DRY), но не type safety. Если нет бизнес-требования — не тратить время.
Единственный реальный фикс
Если Prisma когда-нибудь добавит typed JSON fields:
// Гипотетический синтаксис (не существует в Prisma)
model Season {
rewards Json @type(SeasonRewards)
}
Тогда все касты исчезнут автоматически. До тех пор — касты допустимы.