1. Giris
Siz ilk defe kotlinx.serialization ile tanis olanda bele bir hiss yarana bilerdi: “Ela! Indi men istediyim JSON-u data class-a cevirirem ve her sey hazirdi”. Praktikada ise heyat, hemişe oldugu kimi, biraz daha hiyləgərdir: bezen hesabat formati sizin modellerinizle ust-uste dusmur, bezen yalniz sahələrin bir hissəsini qaytarmaq lazim olur, bezen ise modelede umumen olmayan aqreqatlar elave etmek gerekir. Ve mehz onda JSON-u el ile yigmaq “uf, kostil” olmaqdan cixir ve “oo, rahat alət”e cevrilir.
Bizim praktik konsol layihəmizi tesəvvür edin (serti xerc trekeri): proqram daxilinde normal Kotlin strukturlari var (Expense, siyahilar, map-lar) ve biz “hesabat ixraci”ni JSON-a cevirib istediyimiz her cur inteqrasiya ucun vermek isteyirik. Amma hesabat — bir dənə Expense deyil, təxminən bele bir strukturdur:
- umumi mebleg,
- kateqoriyalar uzre meblegler,
- emeliyyatlar siyahisi,
- xidmeti bolme meta (format versiyasi, valyuta, hesabat muellifi).
Eger “sadəcə xerc siyahisini serializasiya edek” desek, aqreqatlari itireceyik. Eger “bir dənə nəhəng data class Report duzeldək” desek, o surətlə şişməyə başlayacaq, hesabat formati ise oz heyatini yashayacaq ve sizin domen modelinizi pozacaq. Buna gore bu gun JSON-u konstruktor kimi qurmagı oyrenirik: səliqəli, proqnozlasdirila bilən ve setirleri birlesdirmeden (setirleri birlesdirmek — vergullerin unuduldugu səltənətə gedən yoldur).
2. Mentall model: JSON-hesabat bir payp-layn kimi
Kod yazmazdan once, beynimizle razilasmaq faydalidir: burada iki ferqli dunya var — “hesablamalar” ve “JSON-a qablasdirma”. Onlari bir nəhəng funksiya daxilinde qarishdirsaq, sup alinacaq: dadli olmayacaq, debug ise çömçə ilə olacag. Ona gore sadə payp-layn saxlayiriq: evvelce Kotlin-də ededleri hesablayiriq, sonra JSON agacina qablasdiririq.
Budur, mantra kimi təkrar edəcəyimiz sxem (he, proqramcilar da mantrani sevir — sadəcə ona “pattern” deyirler):
flowchart TD
A[Kotlin-də məlumatlar: List⟨Expense⟩] --> B[Hesablamalar: total, byCategory]
B --> C[JsonElement yigma: buildJsonObject/buildJsonArray]
C --> D[Setire kodlama: Json.encodeToString]
D --> E[Cixis/yadda saxlama]
Kotlin baximindan, buildJsonObject { ... } ve buildJsonArray { ... } — resiverli lambdalar uzerinde DSL-builder-lərdir: blokun daxilinde siz sanki “yigan obyektin icindesiniz” ve onun metodlarini cagirirsiniz. “Kapotun altinda” kompilyator tipleri ele cixarmaq ucun elinden geleni edir ki, siz daha az “artiq soz” yazasiniz — bu, apply/with ve diger “idiomatik” konstruksiyalarda olan eyni ruhdur. Daha dərin baxsaq, Kotlin-də bele builder-DSL-lər lambdalarin daxilinde tip cixaris mexanizmlerine (builder-style inference) baglidir.
3. Hazirliq: JsonElement, modeller ve konverterler
JSON-u el ile yigmanin esas “kərpic”ləri
JSON-u el ile yigmaq ucun “men JSON yaziram” fikrinden çıxmaq ve “men JsonElement yaradıram” fikrine keçmek vacibdir. Bu, sintaksis xətasi etmək ehtimalini ciddi azaldir, cunki xetalar “setir xetasi”na deyil (istifadeci danisir), “tip xetasi”na cevrilir (kompilyator danisir).
Asagida — bu gun bizə lazım olan minimal dəst:
| Nə almaq istəyirik | Bunu nə ilə təqdim edirik |
|---|---|
JSON-obyekt |
→ |
JSON-massiv |
→ |
| Setir/eded/boolean | |
|
|
Gəlin cox qisa bir numune ile baslayaq ki, sintaksisi hiss edek ve qorxmayaq. Burada domen mentiqi yoxdur, yalniz “obyekti yigdiq → cap etdik”.
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
fun main() {
val obj: JsonObject = buildJsonObject {
put("status", JsonPrimitive("ok"))
put("count", JsonPrimitive(3))
put("note", JsonNull)
}
println(obj) // {"status":"ok","count":3,"note":null}
}
Diqqət edin: put JsonElement qebul edir, deməli “xam” Int ve String-i JsonPrimitive-e ceviririk. Beli, bu bir az daha çox söz deməkdir — amma son derece şəffafdır.
Layihə modelləri: Expense ve hesabat ucun melumatlar
Numuneler “vakuumda” olmasin deye, gəlin praktik layihə xəttini davam etdirək. Tutaq ki, bizdə xərc var: identifikator, ad, kateqoriya və məbləğ (valyutanin tam vahidlərində ki, kəsrlər və yuvarlaqlaşdırma üstündə mübahisə etməyək).
Əgər sizin layihənizdə sahələr bir az başqa cür adlanırsa — problem deyil: bu gün vacib olan konkret adlar deyil, yanaşmadır.
data class Expense(
val id: Int,
val title: String,
val category: String,
val amount: Int
)
Indi ise kiçik bir xərclər siyahisi hazirlayaq ki, hesabatı birbaşa main daxilinde qura bilək. Real layihədə siyahı sizin saxlanma yerinizdən (fayl/yaddaş) gələcək, amma bizə indi daha vacib olan “JSON-u necə yığmaqdır”.
fun sampleExpenses(): List<Expense> = listOf(
Expense(id = 1, title = "Qehve", category = "Yemek", amount = 220),
Expense(id = 2, title = "Yolpulu", category = "Neqliyyat", amount = 75),
Expense(id = 3, title = "Pitsa", category = "Yemek", amount = 540)
)
Bir entiti ucun JSON: expenseToJson(...)
Hesabatlar quranda demək olar ki, həmişə təkrarlanan hissə olur: “bir element JSON-da necə görünür”. Bunu funksiyaya çıxarmasanız, kod boyunca put("id", JsonPrimitive(...)) tipli kopipasta yayılacaq — sonra formatı bir yerdə yeniləyəcəksiniz, amma digər yerdə unudacaqsınız. Bu, “məndə iki versiya həqiqət var” adlı klassik xətadır: fəlsəfəsiz, amma bug-larla.
Gəlin bir xərci JsonObject-ə çevirən kiçik konverter yazaq. O, “sehrli” şəkildə serializasiya etmir, sahələri açıq şəkildə qoyur — və bu üstünlükdür, çünki siz formatı tam nəzarətdə saxlayırsınız.
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
fun expenseToJson(e: Expense): JsonObject = buildJsonObject {
put("id", JsonPrimitive(e.id))
put("title", JsonPrimitive(e.title))
put("category", JsonPrimitive(e.category))
put("amount", JsonPrimitive(e.amount))
}
Bu andan etibarən hesabat LEGO kimi yığılacaq: xərclər siyahısını expenseToJson vasitəsilə JSON-obyektlər massivinə çeviririk və hər dəfə sahələri yenidən yazmırıq.
4. Hesabatı yığırıq: aqreqatlar ve element massivi
Indi en maraqli hissə: hesabat çox vaxt “hamısı bir yerdə” olur. Və bu, qorxulu deyil — ta ki, məbləğləri birbaşa buildJsonObject daxilində hesablamaga çalışmayasınız. Biz “insanca” edəcəyik: əvvəl hesablayacağıq, sonra qablaşdıracağıq.
Aqreqatlari Kotlin-də hesablayiriq
Evvelce umumi mebleg:
fun totalAmount(items: List<Expense>): Int =
items.sumOf { it.amount }
Sonra kateqoriyalar üzrə məbləğlər. Bəli, bunu müxtəlif yollarla etmək olar; biz sadə və oxunaqlı variantı götürək: qruplaşdırdıq, sonra hər qrupu cəmlədik.
fun totalByCategory(items: List<Expense>): Map<String, Int> =
items.groupBy { it.category }
.mapValues { (_, list) -> list.sumOf { it.amount } }
Aqreqatlari JSON-a qablasdiririq
Indi yekun JsonObject yigiriq. Daxilinde total, byCategory obyekti ve items massivi olacaq.
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
fun buildExpensesReport(items: List<Expense>): JsonObject {
val total = totalAmount(items)
val byCat = totalByCategory(items)
return buildJsonObject {
put("total", JsonPrimitive(total))
put("byCategory", buildJsonObject {
for ((category, amount) in byCat) {
put(category, JsonPrimitive(amount))
}
})
put("items", buildJsonArray {
for (e in items) add(expenseToJson(e))
})
}
}
Burada vacib olanı hiss edin: byCategory — ic-ice JSON-obyektdir, items ise ic-ice JSON-massivdir. Biz “JSON cap etmirik”, element agaci qururuq.
5. Standart olmayan hesabat strukturlari
Hesabatlar nadir hallarda “verilenler bazasinin kopyasi” olur. Adeten format nese qeribe bir sey isteyir: acarlarin adini deyismek, sahələri birləşdirmək, şərti bölmələr, ya da modeldə olmayan daxiletmə (nesting). El ile yigma mehz bunun ucundur: siz formatı domen modelinə zorla “geydirmeden” birbaşa kodda təsvir edə bilərsiniz.
Acarlarin adinin deyisilmesi ve “insanvari” sahələr
Tutaq ki, hesabatda title yox, name istəyirik, üstəlik amountText (setir kimi) sahəsi də olsun — çünki UI-da göstərmək daha rahatdır (bəli, bu mübahisəlidir, amma bəzən hesabatları belə də edirlər).
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
fun expenseToPublicJson(e: Expense): JsonObject = buildJsonObject {
put("id", JsonPrimitive(e.id))
put("name", JsonPrimitive(e.title))
put("amount", JsonPrimitive(e.amount))
put("amountText", JsonPrimitive("${e.amount} ₣ "))
}
Diqqət edin: domen modeli dəyişmədi, amma hesabat formatı dəyişdi. Bu, yaxşı əlamətdir: siz ixrac naminə biznes məntiqini “sındırmırsınız”.
JsonNull vs sahənin olmamasi
Cox rast gelinen incelik: “saha yoxdur” ve “saha var, amma null” — eyni sey deyil. Sizə eyni kimi görünsə belə, xəttin o biri ucunda (insan ya da servis) bunu başqa cür saya bilər. Ona görə biz hər iki variantı edə bilməliyik.
comment sahəsini imitasiya edək: bəzən var, bəzən yoxdur.
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
fun expenseWithCommentJson(e: Expense, comment: String?): JsonObject = buildJsonObject {
put("id", JsonPrimitive(e.id))
put("title", JsonPrimitive(e.title))
if (comment == null) {
put("comment", JsonNull) // saha var, amma null-dur
} else {
put("comment", JsonPrimitive(comment))
}
}
Indi ise “şərh yoxdursa — sahə ümumiyyətlə olmasın” variantı. Bu başqa müqavilədir (contract).
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
fun expenseWithOptionalCommentJson(e: Expense, comment: String?): JsonObject = buildJsonObject {
put("id", JsonPrimitive(e.id))
put("title", JsonPrimitive(e.title))
if (comment != null) {
put("comment", JsonPrimitive(comment)) // comment == null olarsa saha yoxdur
}
}
Xaricden baxanda bu JSON-lar demək olar ki, eyni görünür, amma məna fərqlidir. Və məhz belə fərqlər — el ilə yığmanın bəzən “necə alındısa” avto-serializasiyadan daha dürüst olmasının səbəblərindən biridir.
6. Hibrid yanaşma: bir hissə əl ilə, bir hissə encodeToJsonElement(...) ilə
Bəzən siz hesabatın “çərçivəsini” əl ilə yığmaq istəyirsiniz, amma daxili hansısa bölməni olduğu kimi serializasiya etmək daha sərfəlidir — çünki orada hər şey artıq @Serializable ilə gözəl təsvir edilib. Bu normal strategiyadır: siz “yalnız əl ilə” və “yalnız data class” arasında seçim etməyə məcbur deyilsiniz.
Hesabat üçün kiçik meta-məlumat yazaq (format versiyası və valyuta). Bu sadə və stabil hissədir — @Serializable üçün əla namizəddir.
import kotlinx.serialization.Serializable
@Serializable
data class ReportMeta(
val formatVersion: Int,
val currency: String
)
Indi meta bölməsini Json.encodeToJsonElement(...) ilə qablaşdıraq, qalanını isə əl ilə yığaq.
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.encodeToJsonElement
fun buildReportWithMeta(items: List<Expense>): JsonObject {
val meta = ReportMeta(formatVersion = 1, currency = "RUB")
val total = totalAmount(items)
return buildJsonObject {
put("meta", Json.encodeToJsonElement(meta))
put("total", JsonPrimitive(total))
}
}
Bu niyə rahatdır? Çünki ReportMeta — “kiçik məlumat modeli”dir və onu yenidən istifadə etmək olar. Hesabatın özü isə “standart olmayan struktur”dur və onu əl ilə nəzarətdə saxlamaq daha rahatdır.
7. JsonElement-i setire nece cevirmek olar
Siz JsonObject və ya JsonArray yığandan sonra sadə sual yaranır: “Yaxşı, normal JSON-mətni necə alım?”. Kotlin-də adam .toString() yazmaq istəyir — və bəli, o JSON təmsilini verəcək. Amma demək olar ki, həmişə bu bir sətrlik kompakt yazı olacaq. Loglar üçün normaldır, amma insanın oxuduğu hesabatlar üçün adətən “gözəl” format istəyirsən.
Iki variant edək: kompakt və pretty.
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.serializer
fun main() {
val report: JsonObject = buildJsonObject { put("status", JsonPrimitive("ok")) }
println(report.toString()) // {"status":"ok"}
val prettyJson = Json { prettyPrint = true }
val text: String = prettyJson.encodeToString(JsonElement.serializer(), report)
println(text) // gozel coxsetirli JSON
}
Vacib məqam: biz məhz JsonElement-i kodlayiriq, çünki hesabatımız artıq JSON ağacıdır, @Serializable-klass deyil. Bu məntiqlidir: bu gün biz JSON-u “model kimi” yox, “struktur kimi” qururuq.
8. JSON-u el ile yiganda tipik sehvler
Səhv №1: JSON-u setirlerle birlesdirmek.
Əliniz "{\"total\": $total}" yazmaga gedirsə, yadınıza salın ki, siz artıq JsonObject qura bilirsiniz. Setir birlesdirme dırnaq işarələrində, sətir keçidlərində, escape etmədə və “burada vergül lazımdırmı?” sualında sınır. buildJsonObject ilə heç olmasa kompilyator sizin müttəfiqinizdir, laqeyd müşahidəçi yox.
Səhv №2: hesabat hesablamasini ve JSON-a qablasdirmani bir yerde qarishdirmaq.
Əgər buildJsonObject { ... } daxilində groupBy, sumOf, sortlama və filtrlər etməyə başlasanız, kod tez “kombayn”a çevrilir. Əvvəl hər şeyi Kotlin dəyişənlərinə hesablayın (val total, val byCat), sonra sakitcə qablaşdırın. Beləliklə həm hesablamaları, həm də formatı daha rahat debug edəcəksiniz.
Səhv №3: “saha yoxdur” ile “saha = null”u qarishdirmaq.
Əgər siz bəzən açarı buraxır, bəzən isə JsonNull qoyursunuzsa, xarici istehlakçı fərqli davranmağa başlaya bilər. Bu “xırdalıq” deyil, format müqaviləsinin bir hissəsidir. Qaydanı bir dəfə seçin və hər yerdə ona əməl edin.
Səhv №4: layihə boyunca acar adlarini setir kimi dublikat yazmaq.
Bu gün "total" yazdınız, sabah "Total", o biri gün "sum" — və nəticədə hesabatınız “bəzən oxunur”. Kiçik tədris layihəsində belə, açarları bir yerdə saxlamaq daha yaxşıdır (məsələn, object ReportKeys { const val TOTAL = "total" }) və təsadüfi yazı xətaları artmasın deyə konstantlardan istifadə edin.
Səhv №5: obyekte JsonElement olmayan sey qoyub “demek olar ki, JSON” etmek.
Bəzən yeni başlayanlar put("count", 3) etməyə çalışır və niyə kompilyatorun irad bildirdiyinə təəccüblənir. Əl ilə yığmada siz JsonElement səviyyəsində işləyirsiniz, ona görə ədədləri və sətirləri JsonPrimitive-ə çevirin. Bəli, bir-iki simvol daha uzundur, amma format API səviyyəsində ciddi şəkildə tipləşdirilmiş olur.
Səhv №6: hesabatı bir anda “ideal universal format” kimi etmek.
Hesabat — bu müqavilədir. Bu gün sizə total və items lazımdır, sabah byCategory əlavə olunacaq, o biri gün meta. Əl ilə yığmanın üstünlüyü odur ki, formatı nöqtəvi dəyişmək olur, amma bu o demək deyil ki, “birdən lazım olar” deyə hər şeyi ora doldurmaq gərəkdir. Yalnız real istifadə olunanı edin və ehtiyac yarandıqca genişləndirin.
GO TO FULL VERSION