Tarkibga o'tish

Validation (go-playground/validator)

Validatsiya — kelgan ma’lumot to‘g‘ri va mantiqiy ekanini tekshirish. JSON sintaksisi to‘g‘ri bo‘lishi mumkin, lekin ichidagi qiymatlar noto‘g‘ri bo‘lishi mumkin:

{"email": "salom", "age": -5, "password": "123"}

Bu so‘rov Request body parsing darsidagi decodeJSON tekshiruvidan muvaffaqiyatli o‘tadi. Lekin salom email emas, yosh manfiy bo‘lolmaydi, 123 esa juda qisqa parol.

Bunday tekshiruvlarni if bilan yozish mumkin. CRUD loyihasida aynan shunday qilgan edik. Lekin maydonlar ko‘paygan sari iflar ham ko‘payadi, kod takrorlanadi va o‘qish qiyinlashadi. Go ekotizimida bu ish uchun keng qo‘llanadigan kutubxona bor: github.com/go-playground/validator. Bu kutubxona Gin framework’ining ichida ham ishlatiladi.

O‘rnatish

go get github.com/go-playground/validator/v10

Birinchi misol

Qoidalar struct maydonlariga validate tagi orqali yoziladi:

main.go
package main

import (
    "fmt"

    "github.com/go-playground/validator/v10"
)

type SignupInput struct {
    Name     string `validate:"required,min=2,max=50"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"gte=14,lte=120"`
    Password string `validate:"required,min=8"`
}

func main() {
    validate := validator.New(validator.WithRequiredStructEnabled())

    in := SignupInput{Name: "A", Email: "salom", Age: -5, Password: "123"}

    err := validate.Struct(in)
    if err != nil {
        for _, fe := range err.(validator.ValidationErrors) {
            fmt.Printf("%s: %s qoidasi buzildi (param=%q)\n", fe.Field(), fe.Tag(), fe.Param())
        }
    }
}
Name: min qoidasi buzildi (param="2")
Email: email qoidasi buzildi (param="")
Age: gte qoidasi buzildi (param="14")
Password: min qoidasi buzildi (param="8")

Kod qanday ishladi:

  • validator.New() validator obyektini yaratadi. U birinchi ishlatilganda har bir struct turi haqidagi ma’lumotni keshlaydi. Shu sababli uni bir marta yaratib, butun dastur davomida qayta ishlatish kerak.
  • WithRequiredStructEnabled() — ichki struct maydonlarida required tagining to‘g‘ri ishlashini yoqadi. Kutubxonaning keyingi asosiy versiyasida bu xatti-harakat standart bo‘lishi rejalashtirilgan, shuning uchun uni hozirdan yoqib qo‘yish tavsiya etiladi.
  • validate.Struct(in) barcha qoidalarni tekshiradi va buzilganlarini validator.ValidationErrors ko‘rinishida qaytaradi.
  • Har bir FieldErrordan maydon nomi (Field), qoida nomi (Tag) va qoida parametri (Param) olinadi.

Ko‘p ishlatiladigan qoidalar

Qoidalar vergul bilan ajratiladi va chapdan o‘ngga tekshiriladi.

Qoida Ma’nosi Misol
required Nol qiymat bo‘lmasligi kerak validate:"required"
omitempty Bo‘sh bo‘lsa, keyingi qoidalar tekshirilmaydi validate:"omitempty,url"
min, max Satr uzunligi, son qiymati yoki slice uzunligi validate:"min=3,max=100"
len Aniq uzunlik validate:"len=9"
gt, gte, lt, lte Katta, katta yoki teng, kichik, kichik yoki teng validate:"gte=0"
oneof Ro‘yxatdagi qiymatlardan biri validate:"oneof=draft published"
email Email formati validate:"email"
url, http_url URL formati validate:"http_url"
uuid4 UUID v4 validate:"uuid4"
numeric, alphanum Faqat raqam, faqat harf va raqam validate:"numeric"
eqfield Boshqa maydonga teng validate:"eqfield=Password"
dive Slice yoki map elementlarini tekshirish validate:"dive,min=2"

Info

Satrlar uchun min va max baytlarni emas, belgilarni (rune) sanaydi. "O‘tkir" satri 8 bayt, lekin 6 belgi. Shuning uchun max=6 qoidasidan o‘tadi.

required va nol qiymat

required qiymat o‘z turining nol qiymatiga teng emasligini tekshiradi: satr uchun "", son uchun 0, ko‘rsatkich uchun nil. Bu ba’zan muammo tug‘diradi.

Masalan, Discount int \validate:"required"`maydonida0qiymatini yuborib bo‘lmaydi, chunki0nol qiymat. Chegirma0` bo‘lishi mumkin bo‘lsa, maydonni ko‘rsatkich qiling:

Discount *int `json:"discount" validate:"required,gte=0,lte=100"`

Endi required faqat maydon yuborilganini (nil emasligini) tekshiradi. gte va lte esa ko‘rsatkich ko‘rsatayotgan qiymatga qo‘llanadi.

Ichki struct va slice’lar

type Address struct {
    City   string `json:"city" validate:"required"`
    Street string `json:"street" validate:"required"`
}

type OrderInput struct {
    Address Address  `json:"address" validate:"required"`
    Items   []Item   `json:"items" validate:"required,min=1,max=50,dive"`
    Tags    []string `json:"tags" validate:"max=5,dive,min=2,max=20"`
}
  • Address ichidagi maydonlar avtomatik tekshiriladi.
  • dive dan oldingi qoidalar slice’ning o‘ziga, keyingilari esa har bir elementga qo‘llanadi. Tags uchun: ko‘pi bilan 5 ta teg, har biri 2 dan 20 belgigacha.

JSON maydon nomlarini ishlatish

Xatolarda Go maydon nomi chiqadi: Email, Password. Klient esa JSON nomlarini biladi: email, password. Validatorga JSON tagidagi nomni ishlatishni o‘rgatamiz:

validate.RegisterTagNameFunc(func(f reflect.StructField) string {
    name := strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
    if name == "-" {
        return ""
    }
    return name
})

Endi fe.Field() email qaytaradi. Ichki maydonlar uchun fe.Namespace() to‘liq yo‘lni beradi, masalan OrderInput.address.city.

O‘zbekcha xato xabarlari

fe.Tag() va fe.Param() asosida o‘zimizning xabarlarimizni tuzamiz:

func message(fe validator.FieldError) string {
    switch fe.Tag() {
    case "required":
        return "majburiy maydon"
    case "email":
        return "email formati noto‘g‘ri"
    case "min":
        if fe.Kind() == reflect.String {
            return fmt.Sprintf("kamida %s ta belgi bo‘lishi kerak", fe.Param())
        }
        return fmt.Sprintf("kamida %s bo‘lishi kerak", fe.Param())
    case "max":
        if fe.Kind() == reflect.String {
            return fmt.Sprintf("ko‘pi bilan %s ta belgi bo‘lishi kerak", fe.Param())
        }
        return fmt.Sprintf("ko‘pi bilan %s bo‘lishi kerak", fe.Param())
    case "gte":
        return fmt.Sprintf("%s dan kichik bo‘lmasligi kerak", fe.Param())
    case "lte":
        return fmt.Sprintf("%s dan katta bo‘lmasligi kerak", fe.Param())
    case "oneof":
        return fmt.Sprintf("quyidagilardan biri bo‘lishi kerak: %s", fe.Param())
    case "eqfield":
        return fmt.Sprintf("%s maydoniga teng bo‘lishi kerak", fe.Param())
    case "uzphone":
        return "telefon raqami +998XXXXXXXXX formatida bo‘lishi kerak"
    default:
        return "qiymat noto‘g‘ri"
    }
}

fe.Kind() maydon turini beradi. min=8 satr uchun "8 ta belgi", son uchun esa "8 dan kichik emas" degan ma’noni bildiradi. Xabar ham shunga qarab tanlanadi.

Kutubxonada tayyor tarjima tizimi ham bor: go-playground/universal-translator paketi. Lekin unda o‘zbek tili uchun tayyor tarjimalar yo‘q. Bir nechta qoida uchun yuqoridagi switch ancha sodda va tushunarli.

Maxsus qoida qo‘shish

Tayyor qoidalar yetmasa, o‘zingiz yozasiz. O‘zbekiston telefon raqami uchun uzphone qoidasini qo‘shamiz:

var uzPhoneRe = regexp.MustCompile(`^\+998\d{9}$`)

validate.RegisterValidation("uzphone", func(fl validator.FieldLevel) bool {
    return uzPhoneRe.MatchString(fl.Field().String())
})

Endi uni boshqa qoidalar kabi ishlatish mumkin:

Phone string `json:"phone" validate:"required,uzphone"`

fl.Field() tekshirilayotgan maydon qiymatini reflect.Value ko‘rinishida beradi. Funksiya true qaytarsa, qiymat to‘g‘ri hisoblanadi.

To‘liq misol: API’da validatsiya

main.go
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
    "reflect"
    "regexp"
    "strings"

    "github.com/go-playground/validator/v10"
)

// ---------- Validator ----------

var (
    validate   = newValidator()
    uzPhoneRe  = regexp.MustCompile(`^\+998\d{9}$`)
    errInvalid = errors.New("validatsiya xatosi")
)

func newValidator() *validator.Validate {
    v := validator.New(validator.WithRequiredStructEnabled())

    v.RegisterTagNameFunc(func(f reflect.StructField) string {
        name := strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })

    v.RegisterValidation("uzphone", func(fl validator.FieldLevel) bool {
        return uzPhoneRe.MatchString(fl.Field().String())
    })

    return v
}

func message(fe validator.FieldError) string {
    switch fe.Tag() {
    case "required":
        return "majburiy maydon"
    case "email":
        return "email formati noto‘g‘ri"
    case "min":
        if fe.Kind() == reflect.String {
            return fmt.Sprintf("kamida %s ta belgi bo‘lishi kerak", fe.Param())
        }
        return fmt.Sprintf("kamida %s bo‘lishi kerak", fe.Param())
    case "max":
        if fe.Kind() == reflect.String {
            return fmt.Sprintf("ko‘pi bilan %s ta belgi bo‘lishi kerak", fe.Param())
        }
        return fmt.Sprintf("ko‘pi bilan %s bo‘lishi kerak", fe.Param())
    case "gte":
        return fmt.Sprintf("%s dan kichik bo‘lmasligi kerak", fe.Param())
    case "lte":
        return fmt.Sprintf("%s dan katta bo‘lmasligi kerak", fe.Param())
    case "oneof":
        return fmt.Sprintf("quyidagilardan biri bo‘lishi kerak: %s", fe.Param())
    case "eqfield":
        return fmt.Sprintf("%s maydoniga teng bo‘lishi kerak", strings.ToLower(fe.Param()))
    case "uzphone":
        return "telefon raqami +998XXXXXXXXX formatida bo‘lishi kerak"
    default:
        return "qiymat noto‘g‘ri"
    }
}

// validateStruct xatolarni "maydon → xabar" map'iga aylantiradi.
func validateStruct(s any) (map[string]string, error) {
    err := validate.Struct(s)
    if err == nil {
        return nil, nil
    }

    var verrs validator.ValidationErrors
    if !errors.As(err, &verrs) {
        return nil, err // noto‘g‘ri ishlatish, masalan struct o‘rniga nil berilgan
    }

    fields := make(map[string]string, len(verrs))
    for _, fe := range verrs {
        // "SignupInput.address.city" → "address.city"
        ns := fe.Namespace()
        if i := strings.Index(ns, "."); i >= 0 {
            ns = ns[i+1:]
        }
        fields[ns] = message(fe)
    }
    return fields, errInvalid
}

// ---------- Kirish ma’lumoti ----------

type Address struct {
    City   string `json:"city" validate:"required,min=2"`
    Street string `json:"street" validate:"required"`
}

type SignupInput struct {
    Name            string   `json:"name" validate:"required,min=2,max=50"`
    Email           string   `json:"email" validate:"required,email"`
    Phone           string   `json:"phone" validate:"required,uzphone"`
    Age             int      `json:"age" validate:"gte=14,lte=120"`
    Role            string   `json:"role" validate:"omitempty,oneof=reader author"`
    Password        string   `json:"password" validate:"required,min=8,max=72"`
    PasswordConfirm string   `json:"password_confirm" validate:"required,eqfield=Password"`
    Interests       []string `json:"interests" validate:"max=5,dive,min=2,max=30"`
    Address         Address  `json:"address"`
}

// ---------- Handler ----------

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func signup(w http.ResponseWriter, r *http.Request) {
    var in SignupInput
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        writeJSON(w, http.StatusBadRequest, map[string]string{"error": "JSON noto‘g‘ri"})
        return
    }

    fields, err := validateStruct(in)
    if errors.Is(err, errInvalid) {
        writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
            "error":  "ma’lumotlar noto‘g‘ri",
            "fields": fields,
        })
        return
    }
    if err != nil {
        log.Printf("validatsiya: %v", err)
        writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "ichki xato"})
        return
    }

    writeJSON(w, http.StatusCreated, map[string]string{"message": "ro‘yxatdan o‘tdingiz", "email": in.Email})
}

func main() {
    http.HandleFunc("POST /signup", signup)
    log.Println("Server: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Noto‘g‘ri ma’lumot yuboramiz:

curl -X POST http://localhost:8080/signup -d '{
  "name": "A",
  "email": "salom",
  "phone": "901234567",
  "age": 10,
  "role": "admin",
  "password": "12345678",
  "password_confirm": "12345679",
  "interests": ["go", "x"],
  "address": {"city": "T"}
}'
{
  "error": "ma’lumotlar noto‘g‘ri",
  "fields": {
    "address.city": "kamida 2 ta belgi bo‘lishi kerak",
    "address.street": "majburiy maydon",
    "age": "14 dan kichik bo‘lmasligi kerak",
    "email": "email formati noto‘g‘ri",
    "interests[1]": "kamida 2 ta belgi bo‘lishi kerak",
    "name": "kamida 2 ta belgi bo‘lishi kerak",
    "password_confirm": "password maydoniga teng bo‘lishi kerak",
    "phone": "telefon raqami +998XXXXXXXXX formatida bo‘lishi kerak",
    "role": "quyidagilardan biri bo‘lishi kerak: reader author"
  }
}

(Javob o‘qish qulay bo‘lishi uchun formatlangan.)

To‘g‘ri ma’lumot:

curl -X POST http://localhost:8080/signup -d '{
  "name": "Aziza",
  "email": "[email protected]",
  "phone": "+998901234567",
  "age": 21,
  "password": "juda-maxfiy-parol",
  "password_confirm": "juda-maxfiy-parol",
  "address": {"city": "Toshkent", "street": "Navoiy ko‘chasi, 5"}
}'
{"email":"[email protected]","message":"ro‘yxatdan o‘tdingiz"}

Natijani tahlil qilamiz:

  • Barcha xatolar bir vaqtda qaytdi. Klient formadagi barcha xatolarni bir urinishda ko‘rsatadi, foydalanuvchi ularni birma-bir tuzatib, qayta-qayta yuborishga majbur bo‘lmaydi.
  • Maydon nomlari JSON’dagidek: password_confirm, address.city, interests[1]. Frontend ularni to‘g‘ridan-to‘g‘ri forma maydonlariga bog‘lashi mumkin.
  • role yuborilmasa, omitempty tufayli tekshirilmaydi. Yuborilsa, faqat reader yoki author bo‘lishi mumkin. admin rolini klient o‘zi tanlab ololmaydi.
  • Status kod — 422 Unprocessable Entity. So‘rov sintaktik jihatdan to‘g‘ri, lekin mazmuni noto‘g‘ri bo‘lgani uchun shu kod tanlandi. Ko‘p API’lar bu holatda 400 ham qaytaradi. Qaysi birini tanlasangiz ham, butun API bo‘ylab izchil bo‘ling.

Warning

Parol uchun max=72 qo‘yilishi tasodifiy emas. bcrypt algoritmi 72 baytdan uzun parollarni qabul qilmaydi. Batafsil Parollarni xeshlash darsida. Belgilar va baytlar soni farq qilishi mumkinligini ham unutmang: kirill harflarining har biri 2 bayt joy egallaydi.

Validatsiya qayerda bo‘lishi kerak?

Validatsiya ikki darajada bo‘ladi:

Daraja Nimani tekshiradi Qayerda
Kirish validatsiyasi Format, uzunlik, majburiy maydonlar Handler yoki kirish DTO’si
Biznes validatsiyasi "Bu email band emasmi?", "Omborda yetarli mahsulot bormi?" Service

validate taglari birinchi darajaga mos keladi. Database’ga murojaat qilishni talab qiladigan tekshiruvlarni validator ichiga yozmang. Ular service qatlamiga tegishli.

Xulosa

  • validator.New()ni bir marta yarating va qayta ishlating.
  • Qoidalar validate tagida vergul bilan yoziladi. dive slice elementlarini, omitempty esa ixtiyoriy maydonlarni boshqaradi.
  • required nol qiymatni rad etadi. 0 yoki false ruxsat etilgan bo‘lsa, ko‘rsatkich turidan foydalaning.
  • RegisterTagNameFunc xatolarda JSON maydon nomlarini ko‘rsatadi.
  • FieldErrorning Tag va Param qiymatlari asosida o‘zbekcha xabarlar tuziladi.
  • RegisterValidation bilan maxsus qoidalar qo‘shiladi.

Keyingi darsda barcha xato javoblarini, jumladan validatsiya xatolarini, yagona standart formatga keltiramiz.