Skip to content

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:

  1. BeforeInsert / BeforeUpdate hook (mutating; whichever branch Save resolved to based on the document's ID)
  2. BeforeSave hook (mutating, runs on both branches)
  3. Struct tag validation (validate tags)
  4. Validate() hook (custom business logic)
  5. 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.