Skip to content

Tanggal di Form Mundur Satu Hari: Payload Ikut Jenis Datanya

Adityo Guni Waluyo

Input date mengirim string, payload lama membawa instan UTC. Begini satu hari hilang begitu saja dan kontrak yang menahannya.

Ringkasan

Tanggal input mundur satu hari gara-gara toISOString ngubah midnight WIB jadi UTC, jadi 20 Desember nyasar jadi 19 di payload. Salahnya bukan backend, tapi cara lama yang bungkus string input jadi objek Date lokal dulu. Fixnya simpel: tempel T00:00:00Z eksplisit ke string input biar tanggalnya utuh pas round-trip.

Daftar acara di dashboard KotaPortal menampilkan sesuatu yang bikin berdebar: admin baru saja menyimpan event dengan tanggal 20 Desember 2026, dan baris yang muncul di tabel menulis 19 Desember 2026. Tidak ada error, tidak ada warning, form bilang sukses. Tanggalnya mundur satu hari dengan tenangnya.

Dugaan pertama saya mengarah ke backend: kiranya parser tanggal Go yang salah membaca zona waktu pengguna. Setelah alur datanya ditelusuri dari browser sampai database, dugaan itu meleset. Yang salah justru cara payload disusun di sisi klien, sejak sebelum request berangkat.

Nilai Input Tanggal Adalah String, Bukan Waktu

Faktanya begini: nilai dari <input type="date"> itu string polos berformat yyyy-mm-dd, tanpa komponen jam sama sekali [1]. Sementara kode lama membungkusnya menjadi objek Date di tengah malam waktu lokal, lalu memanggil toISOString untuk dikirim. Di situ celahnya: toISOString selalu mengonversi ke UTC dan menambahkan akhiran Z [2].

Di browser zona WIB, tengah malam 20 Desember lokal adalah pukul 17.00 UTC tanggal 19 Desember. Hasil serialisasinya: 2026-12-19T17:00:00Z. Kalender di payload mundur satu hari sebelum backend sempat bicara.

// dulu: dibungkus Date, lalu diserialisasi
new Date(value).toISOString()
// "2026-12-20" di browser WIB menjadi "2026-12-19T17:00:00Z"

// sekarang: string input ditempel midnight UTC secara eksplisit
`${value}T00:00:00Z`
// "2026-12-20" tetap "2026-12-20T00:00:00Z"

Backend Menolak Tebakannya

Beda sisi, DTO Go memakai *time.Time. Kontrak JSON Go untuk tipe ini kaku: quoted string RFC 3339 dengan presisi sub-detik, dan proses unmarshal memvalidasi lewat parser RFC 3339 yang ketat [3]. Payload tanggal polos tanpa jam pasti ditolak. Saya memandang kekakuan itu sebagai fitur, bukan hambatan: field yang menyimpan kalender tidak boleh dilayani dengan instan waktu yang kebetulan dekat.

Menariknya, JavaScript sendiri sudah setuju soal ini. Parsing string tanggal polos yyyy-mm-dd memang dianggap UTC, bukan waktu lokal [4]. Yang merusak niat data bukan formatnya, tapi langkah membungkus ke objek Date waktu lokal dulu sebelum menyerah pada UTC.

Bagian yang bikin bug semacam ini licin: dua jam yang dipakai beda definisi. Jam browser mengikuti zona mesin pengguna, jam payload mengikuti aturan format. Selama keduanya bertemu di satu string tanpa kesepakatan, offset hanya menunggu momen. Tanggal 20 Desember disimpan pada jam kerja WIB, dan instan UTC-nya sudah menyentuh 19 Desember. Kalau form itu diisi dari server yang zonanya UTC, bug yang sama takkan pernah muncul. Korbannya selalu pengguna di sebelah barat UTC, dan pengujian lokal di zona timur justru paling mudah menangkapnya.

Saya membiarkan backend tetap ketat, tanpa unmarshaler kustom penerima tanggal polos. Kontrak yang saya pilih: field yang semantiknya kalender dikirim dengan jam yang sengaja dipatok, bukan dengan parser yang melonggarkan validasi. Kalau suatu saat ada field yang memang butuh instan waktu, ia akan tetap lewat jalur RFC 3339 penuh, dan kedua jenis data itu tidak tertukar lagi.

Uji akhirnya tidak mewah: simpan event dengan tanggal 2026-12-20, buka ulang datanya, dan bandingkan string yang kembali. 2026-12-20 masuk, 2026-12-20 keluar, tanpa koreksi manual satu hari di sekitarnya. Pola satu baris itu sekarang jadi bawaan semua form tanggal di proyek.

Sisa pertanyaan saya saat membedah: kenapa bukan backend yang melonggarkan parse-nya saja, supaya payload yyyy-mm-dd polos diterima. Jawabannya soal tanggung jawab. Membiarkan backend menerima dua format sekaligus berarti setiap klien berikutnya harus menebak jenis data yang diminta, dan tebakan itulah yang kelak menggeser hari lagi di zona lain. Satu format untuk satu makna jauh lebih murah dirawat daripada parser yang ramah tapi licin.

Payload Ikut Jenis Datanya

Perbaikannya se simpel itu: templat string eksplisit yang menempelkan T00:00:00Z ke nilai input apa adanya. Tanggal 2026-12-20 dikirim persis sebagai 2026-12-20T00:00:00Z, dan kalendernya selamat melewati round-trip. Pengujian langsung di proyek membuktikan input 2026-12-20 kini tersimpan sebagai 2026-12-20, tanpa pergeseran.

Platform web sendiri pelan-pelan mengakui bahwa tanggal tanpa jam adalah tipe data tersendiri: Temporal.PlainDate memang dirancang untuk kalender tanpa zona waktu, meski dukungan browsernya belum masuk kategori Baseline [5]. Sampai dukungannya merata, menyusun string UTC secara manual adalah pola paling jujur yang bisa saya andalkan: field tanggal dikirim sebagai tanggal, dan Z di akhir string adalah saksi bahwa instan waktu memang sengaja dipatok di midnight UTC.

## Sources [1] https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString [3] https://github.com/golang/go/blob/master/src/time/time.go [4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse [5] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate

Artikel terkait