Validation¶
Den supports two validation mechanisms that can be used independently or combined.
Validator Interface¶
Implement the Validate(ctx context.Context) error method on your document struct for custom business logic validation:
type Article struct {
document.Base
Title string `json:"title"`
Body string `json:"body"`
}
func (a *Article) Validate(ctx context.Context) error {
if a.Title == "" {
return errors.New("title is required")
}
if len(a.Body) < 10 {
return errors.New("body must be at least 10 characters")
}
return nil
}
The Validate(ctx) hook is called automatically before every Save. If it returns an error, the write is aborted. The context carries cancellation, deadlines, and any tracing/auth values from the surrounding call — use it for validators that hit a database, call out to another service, or otherwise need to participate in the request lifecycle.
Struct Tag Validation¶
For structural validation rules, Den integrates with go-playground/validator via the validate package. Tag validation is always-on — any validate:"..." tag is enforced by Den on every Save, no opt-in required:
type User struct {
document.Base
Username string `json:"username" den:"unique" validate:"required,min=3,max=50"`
Email string `json:"email" den:"unique" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
Website string `json:"website" validate:"omitempty,url"`
}
The validate/ package also exports validate.Document(doc) for callers that want to run the same checks outside the Den boundary — typical use is an HTTP handler that rejects bad input before opening a database transaction. The parameter type is document.Document, so it accepts any type that embeds document.Base; passing a non-document struct fails at compile time. For validating arbitrary non-document structs, use go-playground/validator/v10 directly. The returned *validate.Errors mirrors what Den's write path would have produced.
Execution Order¶
Both validation mechanisms run after any mutating BeforeInsert / BeforeUpdate / BeforeSave hook, so hooks can populate default values, compute derived fields, or normalize inputs before the constraints are checked.
The full order during Save:
BeforeInsert/BeforeUpdatehook (mutating; whichever branchSaveresolved to based on the document's ID)BeforeSavehook (mutating, runs on both branches)- Struct tag validation (
validatetags) Validate()hook (custom business logic)- Write to the database
If tag validation fails, the Validate() hook is not called. Either step aborts the write and rolls back the transaction.
Note
This is the same pattern used by ActiveRecord, Django ORM, and SQLAlchemy: hooks first, then validation against the final state. It lets you write things like "BeforeInsert generates the slug from the title; tag validation then requires the slug to be non-empty" without fighting the framework.
Error Handling¶
Validation errors are wrapped with den.ErrValidation:
err := den.Save(ctx, db, &user)
if errors.Is(err, den.ErrValidation) {
fmt.Println("Validation failed:", err)
}
For field-level details from tag validation, use errors.As:
var validationErr *validate.Errors
if errors.As(err, &validationErr) {
for _, fe := range validationErr.Fields {
fmt.Printf("Field %s failed on %s\n", fe.Field, fe.Tag)
}
}
Full Example¶
type User struct {
document.Base
Username string `json:"username" den:"unique" validate:"required,min=3"`
Email string `json:"email" den:"unique" validate:"required,email"`
Bio string `json:"bio" validate:"max=500"`
}
func (u *User) Validate(ctx context.Context) error {
if strings.Contains(u.Username, " ") {
return errors.New("username must not contain spaces")
}
return nil
}
db, _ := den.OpenURL(ctx, "sqlite:///data.db")
den.Register(ctx, db, &User{})
user := &User{Username: "ab", Email: "invalid"}
err := den.Save(ctx, db, user)
// err wraps den.ErrValidation — "ab" is too short, "invalid" is not a valid email
Tip
Use validate tags for structural constraints (required, format, length) and the Validate() hook for business rules that depend on multiple fields or external state.