Aplikasi keuangan tidak berguna jika data hilang setelah program ditutup. Pada Bagian 4 kita menambahkan penyimpanan ke file JSON dengan validasi input yang ketat. Validasi mencakup format tanggal ISO, batas jumlah, dan pola kategori. Tanpa validasi, data yang dimuat kembali bisa rusak dan menyebabkan galat runtime di Bagian 3.
Tiga Lapisan Validasi #
Validasi input terbagi tiga lapisan:
- Format — tanggal ISO
YYYY-MM-DD, kategori hanya huruf kecil. - Batas nilai — jumlah positif dan di bawah satu miliar.
- Konsistensi — ID tidak duplikat, kategori yang dimaksud ada di daftar.
Pemisahan lapisan membuat pesan galat lebih informatif dan kode lebih mudah diuji.
Implementasi Validasi dan Penyimpanan #
Kode berikut menunjukkan validator terpisah dan penyimpanan JSON:
// Bagian 4: Penyimpanan ke file dan validasi input.
// Mendemonstrasikan persistensi JSON, validasi tanggal ISO, dan
// pemuatan kembali dengan struktur yang konsisten.
import java.io.File
import java.time.LocalDate
import java.time.format.DateTimeParseException
enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }
data class Transaksi(
val id: String,
val jenis: JenisTransaksi,
val jumlah: Long,
val tanggal: String,
val kategori: String
)
object Validator {
fun validasiTanggal(tanggal: String): Result<String> {
return try {
LocalDate.parse(tanggal)
Result.success(tanggal)
} catch (e: DateTimeParseException) {
Result.failure(IllegalArgumentException("Format tanggal harus ISO: YYYY-MM-DD"))
}
}
fun validasiJumlah(jumlah: Long): Result<Long> {
if (jumlah <= 0) return Result.failure(IllegalArgumentException("Jumlah harus positif"))
if (jumlah > 1_000_000_000L) return Result.failure(IllegalArgumentException("Jumlah melebihi batas"))
return Result.success(jumlah)
}
fun validasiKategori(kategori: String): Result<String> {
if (kategori.isBlank()) return Result.failure(IllegalArgumentException("Kategori tidak boleh kosong"))
if (!kategori.matches(Regex("^[a-z]+$"))) {
return Result.failure(IllegalArgumentException("Kategori hanya huruf kecil"))
}
return Result.success(kategori)
}
}
object Penyimpanan {
private val RE = Regex("""\{"id":"([^"]+)","jenis":"(PEMASUKAN|PENGELUARAN)","jumlah":(\d+),"tanggal":"([^"]+)","kategori":"([^"]+)"\}""")
fun simpan(file: File, daftar: List<Transaksi>) {
val isi = daftar.joinToString(",") { t ->
val j = if (t.jenis == JenisTransaksi.PEMASUKAN) "PEMASUKAN" else "PENGELUARAN"
"{\"id\":\"${t.id}\",\"jenis\":\"$j\",\"jumlah\":${t.jumlah},\"tanggal\":\"${t.tanggal}\",\"kategori\":\"${t.kategori}\"}"
}
file.writeText("[$isi]")
}
fun muat(file: File): List<Transaksi> {
if (!file.exists()) return emptyList()
return RE.findAll(file.readText()).map { m ->
val (id, jenis, jumlah, tanggal, kategori) = m.destructured
Transaksi(id, JenisTransaksi.valueOf(jenis), jumlah.toLong(), tanggal, kategori)
}.toList()
}
}
fun main() {
println("=== Validasi input ===")
val daftarTanggal = listOf("2026-01-15", "15/01/2026", "2026-13-01")
daftarTanggal.forEach { tgl ->
val r = Validator.validasiTanggal(tgl)
println("Tanggal \"$tgl\": ${r.fold(onSuccess = { "valid" }, onFailure = { it.message })}")
}
val daftarJumlah = listOf(500_000L, -100L, 2_000_000_000L)
daftarJumlah.forEach { jml ->
val r = Validator.validasiJumlah(jml)
println("Jumlah $jml: ${r.fold(onSuccess = { "valid" }, onFailure = { it.message })}")
}
println()
println("=== Penyimpanan dan pemuatan ===")
val file = File("transaksi-demo.json")
file.deleteOnExit()
val data = listOf(
Transaksi("T001", JenisTransaksi.PEMASUKAN, 5_000_000, "2026-01-05", "gaji"),
Transaksi("T002", JenisTransaksi.PENGELUARAN, 750_000, "2026-01-08", "makan"),
Transaksi("T003", JenisTransaksi.PENGELUARAN, 1_200_000, "2026-01-12", "transport")
)
Penyimpanan.simpan(file, data)
println("Disimpan: ${file.length()} byte")
val dimuat = Penyimpanan.muat(file)
println("Dimuat: ${dimuat.size} transaksi")
dimuat.forEach { t ->
val arah = if (t.jenis == JenisTransaksi.PEMASUKAN) "+" else "-"
println("[${t.id}] $arah ${t.jumlah} ${t.tanggal} (${t.kategori})")
}
file.delete()
}Output yang Dihasilkan:
=== Validasi input ===
Tanggal "2026-01-15": valid
Tanggal "15/01/2026": Format tanggal harus ISO: YYYY-MM-DD
Tanggal "2026-13-01": Format tanggal harus ISO: YYYY-MM-DD
Jumlah 500000: valid
Jumlah -100: Jumlah harus positif
Jumlah 2000000000: Jumlah melebihi batas
=== Penyimpanan dan pemuatan ===
Disimpan: 286 byte
Dimuat: 3 transaksi
[T001] + 5000000 2026-01-05 (gaji)
[T002] - 750000 2026-01-08 (makan)
[T003] - 1200000 2026-01-12 (transport)Mari kita bedah komponen utama:
LocalDate.parse(tanggal): parsing tanggal ISO bawaan Java. MelemparDateTimeParseExceptionjika format salah, misalnya15/01/2026atau tanggal yang tidak ada seperti2026-13-01.Regex("^[a-z]+$"): pola kategori hanya huruf kecil.^dan$memastikan seluruh string cocok, bukan hanya sebagian.daftar.joinToString(",") { ... }: menggabungkan entri JSON dengan koma. Setiap entri dibungkus{}dan berisi lima field.RE.findAll(file.readText()).map { ... }.toList(): regex dengan lima grup tangkapan.m.destructuredmembongkar grup ke variabel, sehingga konstruksiTransaksilebih ringkas.JenisTransaksi.valueOf(jenis): konversi string ke enum. MelemparIllegalArgumentExceptionjika nilai tidak ada; untuk produksi, bungkus dengan try-catch dan lempar galat domain.file.deleteOnExit(): mendaftarkan file untuk dihapus saat program keluar. Berguna pada demo agar tidak meninggalkan sampah di folder kerja.
Mengapa Validasi Terpisah #
Memisahkan validator ke object Validator memberi tiga manfaat:
- Testabilitas: setiap fungsi dapat diuji terpisah dari penyimpanan dan I/O.
- Reusabilitas: validator dipakai oleh CLI, REST API, dan test dengan aturan yang sama.
- Keterbacaan: fungsi pendek dengan nama deskriptif (
validasiTanggal,validasiJumlah) lebih jelas daripada satu fungsivalidasiSemuayang panjang.
Round-Trip JSON #
Penyimpanan yang baik memenuhi sifat round-trip: data yang disimpan dan dimuat kembali harus identik. Kode di atas menjaga sifat ini dengan dua aturan:
- Serialisasi dan deserialisasi memakai field yang sama persis (id, jenis, jumlah, tanggal, kategori).
- Tipe data konsisten:
Longuntuk jumlah,Stringuntuk tanggal,JenisTransaksiuntuk jenis.
Jika satu field ditambahkan (misalnya catatan), serialisasi dan deserialisasi harus diperbarui bersamaan; jika tidak, data lama tidak dapat dimuat.
Referensi Lanjutan #
- Kotlin stdlib API reference
— rujukan stdlib Kotlin untuk
require()dan serialisasi JSON sebagai lapisan validasi - Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai titik masuk topik penyimpanan data persisten
Ringkasan Bagian 4 dan Penutup Proyek #
Penyimpanan JSON dan validasi input kini lengkap. Validator memvalidasi tanggal, jumlah, dan kategori secara terpisah. Penyimpanan menyimpan dan memuat transaksi dengan format yang konsisten. Data bertahan antar sesi pemakaian.
Proyek Aplikasi Keuangan Pribadi telah selesai. Kita membangun pencatatan transaksi di Bagian 1, anggaran per kategori di Bagian 2, laporan dan analisis di Bagian 3, serta penyimpanan dan validasi di Bagian 4. Aplikasi kini dapat mencatat, menganalisis, dan menyimpan data keuangan pribadi. Selanjutnya, kita beralih ke proyek REST API Manajemen Tugas yang memperkenalkan Ktor untuk membangun layanan HTTP.