В каком формате метки времени в JSON
Главное: Эпохи и ISO-строки имеют свои компромиссы, но именно их смешивание реально ломает всё. Используйте ISO 8601 со смещением, храните UTC, отображайте в часовом поясе пользователя.

Главное: используйте ISO 8601 с явным смещением в передаче, храните UTC с завершающим Z и конвертируйте только при отображении; если вы вместо этого выбираете числовые эпохи, глобально зафиксируйте секунды против миллисекунд и поместите единицу в имя поля.
Почему 2026-10-30 14:30:00 недостаточно
Эта строка не несёт зону. 14:30 в Шанхае и 14:30 в Лондоне различаются на восемь часов, и нет способа понять, что имеется в виду. new Date('2026-10-30 14:30:00') разбирается в локальной зоне среды выполнения, поэтому сервер и браузер выводят два разных момента из одной полезной нагрузки — тот тип бага, который никогда не воспроизводится.
Голая 2026-10-30 — вовсе не момент. Подходит для дней рождения и периодов оплаты, неверна для времени создания.
Два рабочих формата
Строки ISO 8601
{
"createdAt": "2026-10-30T14:30:00+08:00",
"updatedAt": "2026-10-30T06:30:00Z"
}
Читаемо, сортируемо при согласованном формате, с точностью до миллисекунд при необходимости, и самодокументируемо.
Числовые эпохи
{ "createdAt": 1793519400 }
Компактно и однозначно, но нечитаемо, и секунды против миллисекунд — постоянная угроза. Если выбираете эпохи, стандартизируйте единицу и поместите её в имя поля: createdAtMs гораздо безопаснее, чем createdAt.
Соглашения, которые стоит зафиксировать рано
| Пункт | Соглашение |
|---|---|
| Формат передачи | ISO 8601 с Z или +08:00 |
| Зона хранения | UTC, чтобы летнее время никогда не входило в картину |
| Точность | Миллисекунд достаточно |
| Именование полей | createdAt, updatedAt, expiresAt |
| Чистые даты | Отдельное поле date, например birthday: "1990-01-01" |
| Длительности | Целое плюс единица, например timeoutMs: 3000 |
Отображение
new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Europe/London',
}).format(new Date('2026-10-30T06:30:00Z'));
Указывайте timeZone явно, когда отображение не должно следовать устройству. Полагать на значение браузера по умолчанию правильно для времени, обращённого к пользователю, и неверно для отчётов.
Три повторяющихся бага
Голые даты разбираются как полночь UTC. Согласно спецификации ECMAScript, строки только-даты являются UTC, а строки дата-время — локальными. Эта несогласованность — надёжный источник багов со смещением на день.
Зона базы данных расходится с сериализатором. datetime в MySQL не имеет зоны; запишите локальное, читайте как UTC, и всё сдвигается на смещение.
Летнее время. Хранение будущих локальных времён создаёт моменты, которые не существуют или происходят дважды. В UTC таких пробелов нет.
Вопросы и ответы
Как выбрать между ISO 8601 и числовыми эпохами?
Используйте ISO 8601, когда полезную нагрузку читают люди, когда нужен лексикографический порядок сортировки или когда значение должно нести свою зону; цена — примерно дюжина лишних байтов и библиотека парсинга. Используйте числовые эпохи, когда компактность и тривиальная кросс-языковая обработка важнее, но глобально зафиксируйте секунды против миллисекунд и укажите, какую единицу содержит поле.
Должны ли голая дата и метка времени использовать одно поле?
Нет. Голая 2026-10-30 — это календарный день без времени и без зоны, тогда как поле метки времени должно нести точный момент. Держите дни рождения и периоды оплаты в отдельном поле date, а для времени создания или обновления используйте ISO 8601-поле At.