Optional Slice Validation
Learn how optional slice validation works in NLG Form using OptionalSlice, optional arrays, collection validation, item limits, and reusable validation pipelines.
OptionalSlice allows slice validation rules to run only when a collection contains items.
This is useful for optional arrays, tags, permissions, categories, metadata lists, and partial update APIs where empty collections should be ignored instead of rejected.
Why OptionalSlice Exists
Many APIs contain collections that are optional.
Examples include:
- tags
- categories
- permissions
- feature lists
- metadata arrays
- selected filters
- user groups
- shopping cart items
- labels
- batch operations
Without optional validation, every collection would require additional conditional logic.
OptionalSlice keeps validation pipelines clean and reusable.
TL;DR
| Helper | Description |
|---|---|
optional.OptionalSlice(field, rules...) | Runs nested rules only when the slice contains items |
optional.OptionalSliceWith(field, fn, rules...) | Runs validation only when a custom condition passes |
optional.OptionalSliceLen(field, n, rules...) | Runs validation only when the slice length matches n |
Defining Optional Slice Fields
Optional slice validation starts with typed slice fields.
type OptionalSliceRequest struct {
Tags []string `json:"tags"`
Permissions []string `json:"permissions"`
Ids []int `json:"ids"`
}Field definitions use form.Slice.
OptionalSliceForm := struct {
Tags form.SliceField[OptionalSliceRequest, string]
Permissions form.SliceField[OptionalSliceRequest, string]
Ids form.SliceField[OptionalSliceRequest, int]
}{
Tags: form.Slice[OptionalSliceRequest]("tags", func(r *OptionalSliceRequest) []string {
return r.Tags
}),
Permissions: form.Slice[OptionalSliceRequest]("permissions", func(r *OptionalSliceRequest) []string {
return r.Permissions
}),
Ids: form.Slice[OptionalSliceRequest]("ids", func(r *OptionalSliceRequest) []int {
return r.Ids
}),
}Applying OptionalSlice
OptionalSlice executes nested validation rules only when the slice contains items.
return form.Schema[OptionalSliceRequest]{
optional.OptionalSlice(
OptionalSliceForm.Tags.Field,
rules.MinItems(OptionalSliceForm.Tags, 2),
rules.UniqueItems(OptionalSliceForm.Tags),
),
}Validation behavior:
| Input | Result |
|---|---|
[] | skipped |
["go"] | invalid |
["go", "api"] | valid |
OptionalSliceWith
OptionalSliceWith allows custom conditional collection validation.
Example:
optional.OptionalSliceWith(
OptionalSliceForm.Tags.Field,
func(v []string) bool {
return len(v) > 0
},
rules.UniqueItems(OptionalSliceForm.Tags),
)Typical use cases:
- advanced filtering
- conditional array validation
- staged onboarding
- dynamic API rules
OptionalSliceLen
OptionalSliceLen executes validation only when the slice length matches a specific value.
Example:
optional.OptionalSliceLen(
OptionalSliceForm.Tags.Field,
1,
rules.ContainsItem(OptionalSliceForm.Tags, "featured"),
)Typical use cases:
- conditional tagging
- restricted batch operations
- workflow constraints
- dynamic collection rules
Combining Slice Rules
Optional slice validation works with all regular slice validation rules.
Example:
optional.OptionalSlice(
OptionalSliceForm.Tags.Field,
rules.MinItems(OptionalSliceForm.Tags, 2),
rules.MaxItems(OptionalSliceForm.Tags, 5),
rules.UniqueItems(OptionalSliceForm.Tags),
)This creates reusable collection validation pipelines.
Complete Example
schema.go
package main
import (
"github.com/netlifeguru/form"
"github.com/netlifeguru/form/optional"
"github.com/netlifeguru/form/rules"
)
const (
CodeTagsMinItems = form.Code("tags_min_items")
CodePermissionsUnique = form.Code("permissions_unique")
CodeIdsBetween = form.Code("ids_between")
)
type OptionalSliceRequest struct {
Tags []string `json:"tags"`
Permissions []string `json:"permissions"`
Ids []int `json:"ids"`
}
func OptionalSliceSchema() form.Schema[OptionalSliceRequest] {
OptionalSliceForm := struct {
Tags form.SliceField[OptionalSliceRequest, string]
Permissions form.SliceField[OptionalSliceRequest, string]
Ids form.SliceField[OptionalSliceRequest, int]
}{
Tags: form.Slice[OptionalSliceRequest]("tags", func(r *OptionalSliceRequest) []string {
return r.Tags
}),
Permissions: form.Slice[OptionalSliceRequest]("permissions", func(r *OptionalSliceRequest) []string {
return r.Permissions
}),
Ids: form.Slice[OptionalSliceRequest]("ids", func(r *OptionalSliceRequest) []int {
return r.Ids
}),
}
return form.Schema[OptionalSliceRequest]{
optional.OptionalSlice(
OptionalSliceForm.Tags.Field,
rules.MinItemsWithCode(OptionalSliceForm.Tags, 2, CodeTagsMinItems),
),
optional.OptionalSlice(
OptionalSliceForm.Permissions.Field,
rules.UniqueItemsWithCode(OptionalSliceForm.Permissions, CodePermissionsUnique),
),
optional.OptionalSlice(
OptionalSliceForm.Ids.Field,
rules.ItemsBetweenWithCode(OptionalSliceForm.Ids, 1, 5, CodeIdsBetween),
),
}
}main.go
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"github.com/netlifeguru/form"
"github.com/netlifeguru/form/httpform"
"github.com/netlifeguru/router"
)
func main() {
r := router.New()
r.HandleFunc("/optional-slice", "POST", func(w http.ResponseWriter, req *http.Request, ctx *router.Context) {
var in OptionalSliceRequest
if !httpform.BindAndValidate(w, req, &in, OptionalSliceSchema(), 1<<20) {
fmt.Println("optional slice validation failed")
return
}
fmt.Println("optional slice validation passed:", in)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "optional slice validation passed",
"data": in,
})
})
validPayload := map[string]any{
"tags": []string{"go", "api"},
"permissions": []string{"read", "write"},
"ids": []int{1, 2, 3},
}
invalidPayload := map[string]any{
"tags": []string{"go"},
"permissions": []string{"read", "read"},
"ids": []int{1, 2, 3, 4, 5, 6},
}
skippedPayload := map[string]any{
"tags": []string{},
"permissions": []string{},
"ids": []int{},
}
fmt.Println("\n--- Valid request ---")
form.SendTestPost(":8080/optional-slice", validPayload)
fmt.Println("\n--- Invalid request ---")
form.SendTestPost(":8080/optional-slice", invalidPayload)
fmt.Println("\n--- Skipped validation request ---")
form.SendTestPost(":8080/optional-slice", skippedPayload)
if err := r.ListenAndServe(8080); err != nil {
slog.Error("failed to start server", "error", err)
os.Exit(1)
}
}Validation Flow
Optional slice validation behaves like this:
empty slice
↓
validation skippednon-empty slice
↓
nested rules executedNotes
- Optional slice validation runs only when the slice contains items.
- Empty collections are intentionally skipped and are not validation failures.
- Optional slice validation is useful for PATCH APIs and partial updates.
- Optional slice validation works with all standard slice rules.
- Collection validation remains reusable and transport-independent.
- Optional slice validation helps keep schemas explicit and composable.
- Slice validation is especially useful for APIs that expose optional tags, permissions, metadata, or batch payloads.
Optional Pointer Validation
Learn how optional pointer validation works in NLG Form using OptionalPtr, nullable fields, PATCH APIs, nested objects, and reusable conditional validation pipelines.
Conditional Validation
Learn how conditional validation works in NLG Form using conditional rules, runtime predicates, When helpers, and reusable validation pipelines.