CodeGym /Kurslar /Kotlin SELF /JSON melumatlarinin validasiyasi

JSON melumatlarinin validasiyasi

Kotlin SELF
Səviyyə, Dərs
Mövcuddur

1. Parsinq ve validasiya: rollar ve netice kontrakti

JSON ugurla pars olunsa bele, bu hele o demek deyil ki, melumatlar tetbiqimize uyqundur. Bu leksiyada biz validasiya qati quracagiq: o, JsonElement-i normal Kotlin-melumatlarina (ve ya anlasilan bir xetaya) cevirir ve bunu istisnalari (exceptions) “adi” mentiq idareetme mexanizmi kimi istifade etmeden edir.

Siz “xarici dunyadan” (fayl, istifadeci, bashqa servis) melumat qebul etmeye baslayanda, cox istemek olur ki, en yaxsiya inanasan: “axir orada amount yazilibsa, demeli reqem olacaq”. Realliq adeten bu qeder romantik deyil: sahe olmaya biler, null ola biler, "100" kimi setr ola biler, "sto" kimi setr ola biler, ya da umumen basqa adla gedə biler. Ona gore bize intizam lazimdir: evvel “bu umumen JSON-dur?” sualina cavab veririk, sonra ise — “bu, emal etmeye hazir oldugumuz melumatlardirmi?”.

Parsinq vs validasiya

Parsinq — sintaksisin yoxlanmasidir. Yani “setr JSON qaydalarina uyğundur”. Eger vergul ve ya dırnaq unudulubsa, parser “bacarmiram” deyecek (ve bu normaldir). Validasiya ise menanin ve tetbiq teleblerinin yoxlanmasidir: saheler movcuddur, tiplər dogrudur, deyerler adekvat araliqlardadir, setrler bos deyil, ededler menfi deyil ve s.

Bele bir sxemi yadda saxlamaq rahatdir (bu, bizim funksiyalarin arxitekturasi da olacaq):

flowchart LR
    A["String (JSON text)"] --> B["parseToJsonElement()"]
    B -->|ugur| C["JsonElement (derex)"]
    B -->|xeta| E["Error: 'invalid JSON'"]
    C --> D["validate(...)"]
    D -->|Ok| F["Normal Kotlin-melumatlari"]
    D -->|Error| G["Istifadeci/log ucun aydin xeta"]

Bu ayirmanin esas faydasi ondadir ki, kod proqnozlasdirila bilir olur: bizde JSON-un agaca cevrildiyi bir “noqte”, ve agacin duzgun modele cevrildiyi ayrica bir “noqte” var. Nese duz getmese, biz hansi merhelede ve niye oldugunu deqiq anlayiriq.

Niyə Boolean kifayet deyil ve nece olur ki, Ok/Error lazimdir

Yeni baslayan biri “validasiya” yazanda, cox vaxt bele funksiya alinır: fun isValid(x): Boolean. Bu pis baslangic deyil, amma realliqda islemir: validasiya false qaytaribsa, biz niye oldugunu bilmeliyik. Eks halda tetbiq falciya cevrilir: “melumatlar yanlisdir… hansilar? harada? neyi duzeltmek lazimdir?”.

Ona gore validatora daha “boyuk” kontrakt veririk: ya ugur, ya da mesajli xeta qaytarir. Bu merhelede razilasaq: “birdefeye 17 xetadan ibaret torba” yigmayacagiq, bir sebeb qaytaracagiq — hem kod, hem de istifadeci ucun daha sadedir.

Universal netice tipini quraq:

sealed class ValidationResult<out T> {
    data class Ok<T>(val value: T) : ValidationResult<T>()
    data class Error(val message: String) : ValidationResult<Nothing>()
}

Burada iki meqam vacibdir. Birincisi, Ok artiq hazir, lazim olan tipde (T) deyer saxlayir ki, sonraki kod normal melumatlarla islesin, “JSON-u cubuqla yoxlamağa” davam etmesin. Ikincisi, Error istifadeciye ve ya log-a cixarila bilən mesaj saxlayir (konsol layihesinde cox vaxt bu, demek olar ki, eyni seydir).

Ve daha bir bonus: sealed class when ile ela isleyir — kompilyator bizi butun variantlari emal etmeye mecbur edecek (yani xetani unutmariq ve her seyin yaxsi oldugunu gostermeye calismariq).

2. Mini-sujet: CLI-tetbiqine xerclerin importu

Numuneler “vakumda” qalmasin deye, tedris konsol tetbiqimizi inkisaf etdirmeye davam edek — sert olaraq Expense Tracker (xerclerin ucotu). Daha once biz xercleri saxlayib @Serializable ile JSON-a seriyalasa bilirdik, amma bu gun ssenari ferqlidir: tesəvvur edin ki, bize namelum formatda (ve ya “demek olar ki, bizimki”, amma surprizlerle) JSON gonderilib ve biz onu import etmek isteyirik.

Biz bir xerc vahidini bu sahelerle validasiya edeceyik:

  • title: setr, bos deyil (meselen, "Coffee")
  • amount: tam eded, ciddi sekilde sifirdan boyuk (meselen, 250)
  • category: setr (bos ola biler / olmaya biler — onda "other" qoyariq)

Validasiyadan sonra alacagimiz “artiq normallasdirilmis melumatlar” modelini quraq:

data class ExpenseDraft(
    val title: String,
    val amount: Int,
    val category: String
)

Niyə Draft? Cunki bu, daxil olan melumat validasiyasindan sonra “qaralama”dir: hele saxlanma, ID ve s. haqda danismiriq. Biz sadəcə isteyirik ki, melumatlar normal ve sonrakı mentiq ucun tehlukesiz olsun.

3. Pipeline: JSON-u pars edirik ve melumatlari validasiya edirik

Cox yaygin sehv — hem pars eden, hem validasiya eden, hem cap eden, ve “her ehtimala qarsi” butun istisnalari tutan bir nəhəng funksiya yazmaqdir. O adeten kombayn kimi gorunur: agir, kirilgan ve qorxuludur. Biz daha seliqeli edek: parseJson(text) yalniz metnin duzgun JSON olduguna cavabdehdir; validate... ise struktura ve deyerlerin uyqun olduguna cavabdehdir.

Metni JsonElement agacina parse edirik

parseToJsonElement metn duzgun JSON olmayanda istisna ata biler, ona gore biz xetani duzgun sekilde tutub Error qaytaracagiq:

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement

fun parseJson(text: String): ValidationResult<JsonElement> {
    return try {
        ValidationResult.Ok(Json.parseToJsonElement(text))
    } catch (e: Exception) {
        ValidationResult.Error("Invalid JSON: ${e.message}")
    }
}

Diqqet edin: biz burada hec bir sahe yoxlamasi etmirik: amount/title. Indilik bize yalniz bir sey vacibdir: “agac quruldu, ya yox”.

Helper-funksiyalar: daha az copy-paste, daha cox mena

Validator demek olar ki, hemişe tekrarlanan addimlardan ibaretdir: “saheyi gotur”, “primitiv olduguna emin ol”, “setr temsilini gotur”, “cevir”. Bunu her defe el ile yazsaniz, cox tez JSON-u da, heyati da, klaviaturanı da nifret etmeye baslayacaqsiniz.

Ona gore helper-funksiyalardan yeniden istifade edek:

import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

fun getPrimitiveOrNull(obj: JsonObject, key: String): JsonPrimitive? {
    val el = obj[key] ?: return null
    return el as? JsonPrimitive
}
import kotlinx.serialization.json.JsonObject

fun getStringOrNull(obj: JsonObject, key: String): String? =
    getPrimitiveOrNull(obj, key)?.content
import kotlinx.serialization.json.JsonObject

fun getIntOrNull(obj: JsonObject, key: String): Int? {
    val text = getStringOrNull(obj, key) ?: return null
    return text.toIntOrNull()
}

Burada “felsefe” var: bu funksiyalar istisna atmır ve default deyer qoymur. Onlar sadəcə deyir: “duzgun sekilde cixarabiliremse — cixardim, yoxdursa — null qaytardim”. Artıq validator qerar verir: null burda icazelidir, yoxsa xetadir.

Bir xercin validasiyasi: JsonElement -> Ok(ExpenseDraft) / Error(message)

Indi en dadli hisse: JSON parcasini normal melumatlara ceviririk. Biz obyekt gozleyirik (JsonObject), saheleri yoxlayiriq, mehdudiyyetleri yoxlayiriq ve netice qaytaririq. Yoxlamalarin sirasina riayet etmek vacibdir: evvel struktur (obyektdir ya yox), sonra movcudluq/tip, sonra qaydalar (araliqlar, bos setrler).

import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject

fun validateExpense(el: JsonElement): ValidationResult<ExpenseDraft> {
    val obj = el as? JsonObject ?: return ValidationResult.Error("Expense must be a JSON object")

    val title = getStringOrNull(obj, "title")?.trim()
        ?: return ValidationResult.Error("Field 'title' is required")

    if (title.isBlank()) return ValidationResult.Error("Field 'title' must be non-blank")

    val amount = getIntOrNull(obj, "amount")
        ?: return ValidationResult.Error("Field 'amount' must be an integer")

    if (amount <= 0) return ValidationResult.Error("Field 'amount' must be positive")

    val categoryRaw = getStringOrNull(obj, "category")?.trim()
    val category = if (categoryRaw.isNullOrBlank()) "other" else categoryRaw

    return ValidationResult.Ok(ExpenseDraft(title = title, amount = amount, category = category))
}

Stile diqqet edin: coxlu erken return. Bu “tenbellik” deyil, guard clauses kimi praktik texnikadir: problem tapan kimi — derhal xetani qaytaririq ve “korlanmis” veziyyetde hesablamalari davam etdirmirik.

Bir meqam da var: biz melumatlari normallasdiririq. Setrler ucun trim() edirik, bos/olmayan kateqoriyani ise "other"-e getiririk. Bunun sayesinde kodun davaminda daha az sert var ve bos setr ucbatindan tetbiqi “yixmaq” ehtimali azalir.

Siyahini validasiya edirik: indeksi elave edirik ve ilk xetada dayaniriq

Import demek olar ki, hemişe siyahi kimi gelir: xercler massivi. Burada elave sual yaranir: eger bir element pisdirse, qalanlari ile ne etmek lazimdir? Bu leksiya cercevesinde “fail fast, amma gozel” strategiyasini secirik: xeta gorunen kimi — dayandiririq ve Error qaytaririq, amma mesaja elementin indeksini elave edirik. Bu, melumatlari debug edende xeyli komek edir.

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement

fun validateExpenses(el: JsonElement): ValidationResult<List<ExpenseDraft>> {
    val arr = el as? JsonArray ?: return ValidationResult.Error("Root must be a JSON array")

    val items = mutableListOf<ExpenseDraft>()
    for ((index, itemEl) in arr.withIndex()) {
        when (val r = validateExpense(itemEl)) {
            is ValidationResult.Ok -> items.add(r.value)
            is ValidationResult.Error -> return ValidationResult.Error("Item[$index]: ${r.message}")
        }
    }
    return ValidationResult.Ok(items)
}

Beli, biz “butun xetalari toplamaq” da ederdik, amma bu ayrica (ve daha murəkkeb) texnikadir. Indi ise daha vacib olan odur ki, kod sadə ve proqnozlasdirila bilən qalsin: bir xeta — bir aydin cavab.

Pipeline-ni main-de birlesdiririk: oxunaqliliq “her seyi bir setre” yazmaqdan daha vacibdir

Her sey hazir olanda, “gozel” etmek isterdin: “bir setrde pars etdim, validasiya etdim, davam etdim”. Amma yeni baslayanlar ucun bu, cox vaxt debug etmek cetin olan magiyaya cevrilir. Ona gore pipeline-ni asikar sekilde, araliq val-lar ile yigacagiq. Kod bir az uzun gorunur, amma onu oxumaq da, duzeltmek de asan olur.

import kotlinx.serialization.json.JsonElement

fun main() {
    val text = """
        [
          {"title":"Coffee","amount":250,"category":"food"},
          {"title":"Book","amount":900,"category":"education"}
        ]
    """.trimIndent()

    val parsed: ValidationResult<JsonElement> = parseJson(text)

    when (parsed) {
        is ValidationResult.Ok -> {
            val validated = validateExpenses(parsed.value)
            println(validated) // Ok(value=[ExpenseDraft(...), ...]) ve ya Error(...)
        }
        is ValidationResult.Error -> println(parsed.message) // Invalid JSON: ...
    }
}

Burada mesuliyyetlerin bolunmesi aydin gorunur: parseJson sintaksis ucun cavabdehdir, validateExpenses ise tetbiqin telebleri ucun. Ve bəli, println(validated) neticenin “texniki” gorunusunu cap edir; real CLI-da siz daha dostcanli mesajlar cixardardiniz, amma bu artiq UI dizayni meselesidir, validasiya yox.

4. require() ve check(): harada yerleri var, harada ise — Ok/Error

Bazen telebe haqli olaraq sorusur: “Bütün bu ValidationResult neye lazimdir, axı require(condition) ve check(condition) var?”. Sual yaxsidir, cunki Kotlin dogrudan da onsertlerle avtomatik istisna ata bilir: require() IllegalArgumentException atir, check() ise IllegalStateException atir. Bu rahatdir ve cox vaxt kodu qisaltdirir.

Amma nuance odur ki, siz deqiq neyi yoxlayirsiniz. require() ve check() — ele hallar ucundur ki, proqram islemeye davam etmemelidir, cunki kontrakt pozulub: meselen, proqramci funksiyani sehv cagirdi, ya da obyekt mumkun olmayan veziyyete dusdu. Bele yoxlamalar “daxili” kodda yaxsidir, burada xetalar mentiqde bug demekdir.

Amma xarici melumatlar (kuceden gelen JSON) — proqramdaki bug deyil. Bu, normal heyat situasiyasidir: melumatlar xarab ola biler, natamam ola biler, versiya uyusmazligi ola biler. Ve bele hallarda istisnalari “adi idareetme yolu” kimi istifade etmek cox vaxt kodu daha az oxunaqli edir. Daha yaxsisi ValidationResult.Error("...") qaytarmaq, istifadeciye aydin metn gostermek ve butun proqram yixilmadan importu duzgun sekilde bitirmekdir.

Cox qisa desek: require/check — developer sehvleri ucun, Ok/Error — daxil olan melumat sehvleri ucun.

5. JSON melumatlarinin validasiyasi zamani tipik sehvler

Sehv №1: parsinq ve validasiyanin bir “kombayn” funksiyada qarishdirilmasi.
Bir funksiyada eyni anda parseToJsonElement, sahe yoxlamalari, konsola cap ve kolleksiyaya yazma edildikde, hansi addimda her seyin sinmasini anlamaq cetinlesir. Iki merheleye bolmek — parsinq ve validasiya — kodu seffaf edir: parse olmursa, problem sintaksisdedir; parse olur, amma validator etiraz edirse — problem strukturda ve ya deyerlerdedir.

Sehv №2: agaci oxuyanda mecburi cevirme as ve operatoru !! istifade etmek.
Bele kod, melumatlarda ilk surprize qeder isleyir, sonra ise en uyğunsuz anda yixilir. Validasiyanin deyeri ondadir ki, o dayaniqli olmalidir: as? + aydin Error(...) demek olar ki, hemişe “bu obyekt olmalidir, men bele qerar verdim” yanaşmasindan daha yaxsidir.

Sehv №3: olmayan saheyi helper-funksiyada erkenden default deyerle evezlemek, problemin ustunu ortmek.
Eger helper-funksiyada “amount yoxdur — qoy 0 olsun” edirsinizse, validator əslində yalnis olan yazini “ugurla” kecire biler. Daha tehlukesiz yanaşma: helper null qaytarir, validator bunun xeta olub-olmadigina ve ya icazeli olmamasina qerar verir ve yalniz validator default qoya biler (biz category ucun = "other" etdiyimiz kimi).

Sehv №4: tipleri ve movcudlugu yoxlamadan “biznes qaydalari”ni yoxlamaq.
Eger evvel if (amount <= 0) yazib, sonra amount-un umumen eded olduguna emin olmamisinizsa, ya sertlerden ibaret qarisq bir mentiq alinacaq, ya da istisna. Stabil sira adeten bele olur: struktur → movcudluq → tip → konversiya → deyer mehdudiyyetleri. Belelikle xeta mesajlari aydin ve tekrarlanan olur.

Sehv №5: sebebli netice yerine Boolean qaytarmaq.
false neyi duzeltmek lazim oldugunu izah etmir. Error-daki mesaj — luks deyil, bir hefte sonra gecenin saat 2-də basqasinin JSON-unu analiz edib pichildayanda gelecek “siz”e minimum nəzakətdir: “bunu kim yazib… aha, men”.

1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Sxemasız parser
Sxemasız parser
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Profil taslağı
Profil taslağı
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Tənzimləmələr siyahısı
Tənzimləmələr siyahısı
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION