Machine generated documentation. Contribute to improve quality together.

Understanding Validation & Custom Types in gohandlers

One of the most powerful features of gohandlers is its ability to generate consistent and thorough validation logic—without adding extra boilerplate to your handlers. This is made possible by a combination of validation-aware request structs and support for custom types that encapsulate their own parsing and checking rules.

In this article, we’ll explore how gohandlers manages validation, how to design custom types, and how to apply both in real-world scenarios.

✅ Why Validation Matters

Validation ensures your application doesn’t receive malformed, missing, or inconsistent data. In traditional Go APIs, you often write repetitive if-else blocks like:

if req.Email == "" {
  http.Error(w, "email is required", http.StatusBadRequest)
  return
}

These checks become cumbersome to maintain, especially across many endpoints.

With gohandlers, validation is cleanly defined within the struct and handled via generated Validate() methods—without cluttering your handler logic.

🧩 Auto-Generated Field-Level Validation

gohandlers analyzes your ...Request structs and produces a Validate() method for each, which returns a map[string]error.

Example:

type CreatePetRequest struct {
  Name string `json:"name"`
  Tag  string `json:"tag"`
}

Generated:

func (req CreatePetRequest) Validate() map[string]error {
  errs := map[string]error{}
  if req.Name == "" {
    errs["name"] = errors.New("name is required")
  }
  if req.Tag == "" {
    errs["tag"] = errors.New("tag is required")
  }
  return errs
}

Inside your handler, you simply call:

if errs := req.Validate(); len(errs) > 0 {
  w.WriteHeader(http.StatusUnprocessableEntity)
  json.NewEncoder(w).Encode(errs)
  return
}

No need to repeat yourself.

🧠 Custom Types for Reusable Validation

Validation gets even more powerful when you move logic into custom types. You can define domain-specific types that:

gohandlers detects and uses these interfaces automatically:

Example: Email Type

type Email string

func (e *Email) FromQuery(s string) error {
  *e = Email(s)
  return nil
}

func (e Email) Validate() error {
  if !strings.Contains(string(e), "@") {
    return errors.New("invalid email format")
  }
  return nil
}

Used in a request struct:

type InviteUserRequest struct {
  Email Email `query:"email"`
}

gohandlers will automatically:

🔄 Reusable Validators with pkg/types/basics

gohandlers provides a standard set of reusable wrappers in the types/basics package:

Example: Bounded Integer

type ListPetsRequest struct {
  Limit types.Int `query:"limit"`
}

func init() {
  types.IntRules["limit"] = types.IntRule{
    Min: 1,
    Max: 100,
  }
}

gohandlers will enforce that ?limit=0 returns:

{ "limit": "value must be at least 1" }

These types make it trivial to define reusable validation across many endpoints.

🔗 Cross-Field Validation

While field-level checks cover most use cases, you can also define cross-field validation manually in your Validate() method.

func (req CreateBookingRequest) Validate() map[string]error {
  errs := map[string]error{}
  if req.StartTime.After(req.EndTime) {
    errs["endTime"] = errors.New("endTime must be after startTime")
  }
  return errs
}

You can combine generated validation with your own logic. If you use custom types on each field, gohandlers will still generate the wrapper that aggregates them.

🛑 Handling Optional Fields

If you use a pointer field (*int, *string, etc.), you can determine if a value was supplied and validate accordingly.

type SearchRequest struct {
  Query *string `query:"q"`
  Page  *int    `query:"page"`
}

In your Validate() method:

if req.Page != nil && *req.Page < 1 {
  errs["page"] = errors.New("page must be >= 1")
}

This gives you full control over optional vs. required semantics.

🧪 Testing Validation

Because all validation lives in Validate() methods, it’s easy to unit test:

func TestInviteUserValidation(t *testing.T) {
  req := InviteUserRequest{Email: "invalid-email"}
  errs := req.Validate()
  if errs["email"] == nil {
    t.Fatal("expected email validation error")
  }
}

This separation also allows you to reuse request types across HTTP and non-HTTP contexts, like CLI tools or background jobs.

✅ Best Practices

🔚 Summary

With gohandlers, validation is:

By combining generated validators with domain-specific custom types, gohandlers helps you write APIs that are not only easy to build—but also hard to break.

Validation becomes something you declare, not something you repeat.

✨ Happy validating!