
Configurable and Extensible Data Validation in Go
Web uygulamaları geliştirirken, dışarıdan gelen verilerin doğruluğunu ve tutarlılığını sağlamak kritik bir gerekliliktir. Go ekosisteminde bu ihtiyacı karşılamak için çeşitli doğrulama kütüphaneleri bulunuyor. Bunlardan biri de, esnek kurallar ve okunabilir bir API sunan ozzo-validation paketi. Bu makalede, ozzo-validation’ın sunduğu özellikleri detaylıca inceleyerek, veri doğrulama süreçlerini nasıl daha etkili hale getirebileceğimizi keşfedeceğiz.
Ne öneriyor?
ozzo-validation
aşağıdaki özelliklere sahiptir;
- Verilerin doğrulanması için
struct tags
yerine kod yazmayı önerir. - Hemen hemen bütün türleri destekler.
Validatable
interface’i uygulandığı sürece özel veri türlerini destekler.sql.Valuer
interface’ini uygulayan tüm veri türlerini doğrulayabilir. Örneğin sql.NullString- Hata kodu desteği.
ozzo-validation
paketi temelde bir adet doğrulama kümesi ve iki adet doğrulama yöntemi içerir. Bir değerin nasıl geçerli kabul edileceğini tanımlamak için doğrulama kurallarını kullanacağız ve değeri doğrulamak için validation.Validate()
veya validation.ValidateStruct()
kullanabiliriz.
How to use?
go get github.com/go-ozzo/ozzo-validation
Basit bir örnek ile başlayalım.
package main
import (
"fmt"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
)
func main() {
data := "example"
err := validation.Validate(data,
validation.Required,
validation.Length(5, 100),
is.URL)
fmt.Println(err)
}
$ go run main.go
must be a valid URL
validation.Validate()
kuralları listelendikleri sırayla çalıştırır. Bir kural doğrulamayı geçemezse ona karşılık gelen hatayı döndürür ve kuralların geri kalanını atlar. Değer tüm doğrulama kurallarını geçerse nil
döndürür.
Validate Struct
Bir struct
içerisinde farklı veri tiplerinden çok sayıda field içerebilir. Genellikle bir Rest endpoint
geliştirirken payload
değerini bir data transfer object (dto)
olarak kurgularız. validateStruct
metodu ile bu nesneleri doğrulayacak kurallar yazabiliriz.
Bir veya birden fazla alan tanımlanan kurallara uymuyorsa, kurallara uymayan değerlerin hangi sebeple geçersiz olarak kabul edildiğini açıkca görmek isteriz.
package model
import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
)
type User struct {
Username string
Email string
Password string
}
func (u User) Validate() error {
return validation.ValidateStruct(&u,
validation.Field(&u.Username, validation.Required, validation.Length(5, 100)),
validation.Field(&u.Email, validation.Required, validation.Length(5, 100), is.Email),
validation.Field(&u.Password, validation.Required, validation.Length(5, 100)))
}
User
isimli bir struct
tipinde nesnemiz var. Bu nesne içerisinde userName
email
ve password
için validation rule
yazabilirsiniz.
Şimdi bu user
nesnesini kullanan bir endpoint
geliştirelim.
go get github.com/gofiber/fiber/v2
package model
import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
)
type User struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
func (u User) Validate() error {
return validation.ValidateStruct(&u,
validation.Field(&u.Username, validation.Required, validation.Length(5, 100)),
validation.Field(&u.Email, validation.Required, validation.Length(5, 100), is.Email),
validation.Field(&u.Password, validation.Required, validation.Length(5, 100)))
}
handlers/user_handler.go
package handlers
import (
"github.com/gofiber/fiber/v2"
"ozzo-validation/model"
)
func RegisterUser(c *fiber.Ctx) error {
var user model.User
if err := c.BodyParser(&user); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid JSON format",
})
}
if err := user.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"message": "Successfully registered",
"user": user,
})
}
main.go
package main
import (
"github.com/gofiber/fiber/v2"
"log"
"ozzo-validation/handlers"
)
func main() {
app := fiber.New()
app.Post("register", handlers.RegisterUser)
log.Fatal(app.Listen(":3000"))
}
curl --location 'http://localhost:3000/register' \
--header 'Content-Type: application/json' \
--data '{
"email": "ali.veliexample.com",
"username": "aliveli",
"password": "Secret123"
}'
{"error":"email: must be a valid email address."}
validateStruct()
gerçekleştirildiğinde, kurayla eklendikleri sırayla çalışır. Bir kural başarısız olursa, o field
için hata kaydedilir ve doğrulama bir sonraki field
ile devam eder.
Validate Map
Her zaman alanları belli veri yapıları ile çalışmayabiliriz. Bazen key-value
olarak depolanan dinamik verilerle çalışmamız gerekebilir. Bu durumda validation.Map()
metodunu kullanabiliriz. Birden fazla key
için kurallar olabilir ve tek bir key
için birden fazla kural ekleyebilirsiniz.
package main
import (
"fmt"
"github.com/go-ozzo/ozzo-validation/is"
validation "github.com/go-ozzo/ozzo-validation/v4"
"regexp"
)
func main() {
c := map[string]interface{}{
"Name": "Qiang Xue",
"Email": "q",
"Address": map[string]interface{}{
"Street": "123",
"City": "Unknown",
"State": "Virginia",
"Zip": "12345",
},
}
err := validation.Validate(c,
validation.Map(
// Name cannot be empty, and the length must be between 5 and 20.
validation.Key("Name", validation.Required, validation.Length(5, 20)),
// Email cannot be empty and should be in a valid email format.
validation.Key("Email", validation.Required, is.Email),
// Validate Address using its own validation rules
validation.Key("Address", validation.Map(
// Street cannot be empty, and the length must between 5 and 50
validation.Key("Street", validation.Required, validation.Length(5, 50)),
// City cannot be empty, and the length must between 5 and 50
validation.Key("City", validation.Required, validation.Length(5, 50)),
// State cannot be empty, and must be a string consisting of two letters in upper case
validation.Key("State", validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
// State cannot be empty, and must be a string consisting of five digits
validation.Key("Zip", validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
)),
),
)
fmt.Println(err)
// Output:
// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
}
Conditional Validation
Her zaman model içerisinde bulunan alanların doğruluğunu kontrol etmek istemeyebiliriz. Örneğin, kullanıcının kayıt tipi email
ise email alanını kontrol etmek mantıklıdır. Ancak kullanıcı phoneNumber
ile kayıt olmak istiyorsa o zaman email
alanının doğruluğunu kontrol etmek gereksiz olabilir.
package model
import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
type RegisterDto struct {
Type string `json:"type"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
}
func (rd RegisterDto) Validate() error {
return validation.ValidateStruct(&rd,
validation.Field(&rd.Type, validation.Required, validation.Length(5, 100)),
validation.Field(&rd.Email, validation.When(rd.Type == "email", validation.Required, validation.Length(5, 100), is.Email)),
validation.Field(&rd.Phone, validation.When(rd.Type == "phone", validation.Required, validation.Length(5, 100), is.Digit)))
}
func main() {
registerDto := model.RegisterDto{
Type: "phone",
Email: "213",
Phone: "123",
Password: "123",
}
err := registerDto.Validate()
fmt.Println(err)
}
Output : phone: the length must be between 5 and 100.
Yukarıdaki çıktıda gözüktüğü gibi type
alanını phone
olarak gönderdiğimiz için email
alanı valid
olmamasına rağmen o alan için herhangi bir kural çalışmadı.
Ozzo-validation ile Go’da esnek, okunabilir ve sürdürülebilir doğrulama kuralları oluşturmanın adımlarını aktarmaya çalıştım. Struct doğrulamadan dinamik kurallara, map desteğinden koşullu (conditional) validasyonlara kadar birçok farklı senaryoda kullanım örneklerini vermeye çalıştım. Eğer dikkatinizi çektiyse aşağıdaki linkten daha detaylı inceleyebilirsiniz.
Umarım faydalı olur. 👋