Optional Time Validation
Learn how optional time validation works in NLG Form using nullable time fields, OptionalTime, date ranges, scheduling validation, and reusable time-based validation pipelines.
OptionalTime allows time validation rules to run only when a nullable time.Time value exists.
This is useful for optional scheduling fields, expiration timestamps, booking windows, deadlines, onboarding flows, and PATCH APIs where date fields may be omitted.
Why OptionalTime Exists
Many APIs contain timestamps that are optional.
Examples include:
- optional booking dates
- expiration timestamps
- scheduled publishing
- availability windows
- onboarding deadlines
- reminder scheduling
- delayed processing
- maintenance windows
- optional event ranges
- partial update APIs
Without optional validation, nullable time fields require additional conditional logic throughout the application.
OptionalTime keeps validation explicit and reusable.
TL;DR
| Helper | Description |
|---|---|
optional.OptionalTime(field, rules...) | Runs nested rules only when the time pointer is non-nil |
optional.AfterOpt(field, t) | Requires the optional time value to be after t |
optional.AfterOptWithCode(field, t, code) | Same as AfterOpt with a custom error code |
optional.BeforeOpt(field, t) | Requires the optional time value to be before t |
optional.BeforeOptWithCode(field, t, code) | Same as BeforeOpt with a custom error code |
optional.BetweenTimeOpt(field, min, max) | Requires the optional time value to be within a range |
optional.BetweenTimeOptWithCode(field, min, max, code) | Same as BetweenTimeOpt with a custom error code |
Defining Optional Time Fields
Optional time validation starts with nullable pointer fields.
type OptionalTimeRequest struct {
StartAt *time.Time `json:"start_at"`
EndAt *time.Time `json:"end_at"`
ExpireAt *time.Time `json:"expire_at"`
}Field definitions use form.OptTime.
OptionalTimeForm := struct {
StartAt form.OptTimeField[OptionalTimeRequest]
EndAt form.OptTimeField[OptionalTimeRequest]
ExpireAt form.OptTimeField[OptionalTimeRequest]
}{
StartAt: form.OptTime[OptionalTimeRequest]("start_at", func(r *OptionalTimeRequest) *time.Time {
return r.StartAt
}),
EndAt: form.OptTime[OptionalTimeRequest]("end_at", func(r *OptionalTimeRequest) *time.Time {
return r.EndAt
}),
ExpireAt: form.OptTime[OptionalTimeRequest]("expire_at", func(r *OptionalTimeRequest) *time.Time {
return r.ExpireAt
}),
}Applying Optional Time Rules
OptionalTime executes nested validation rules only when the timestamp exists.
return form.Schema[OptionalTimeRequest]{
optional.OptionalTime(
OptionalTimeForm.StartAt,
optional.AfterOpt(OptionalTimeForm.StartAt, time.Now()),
),
optional.OptionalTime(
OptionalTimeForm.EndAt,
optional.BeforeOpt(OptionalTimeForm.EndAt, maxDate),
),
optional.OptionalTime(
OptionalTimeForm.ExpireAt,
optional.BetweenTimeOpt(OptionalTimeForm.ExpireAt, now, maxDate),
),
}Validation behavior:
| Input | Result |
|---|---|
null | skipped |
| omitted field | skipped |
| valid timestamp | validated |
| invalid range | validation error |
AfterOpt
Requires the optional timestamp to be after a reference time.
Example:
optional.AfterOpt(
OptionalTimeForm.StartAt,
time.Now(),
)Typical use cases:
- future appointments
- booking systems
- scheduled jobs
- delayed execution
BeforeOpt
Requires the optional timestamp to be before a reference time.
Example:
optional.BeforeOpt(
OptionalTimeForm.EndAt,
maxDate,
)Typical use cases:
- expiration limits
- bounded schedules
- release deadlines
- maintenance windows
BetweenTimeOpt
Requires the optional timestamp to stay within a time range.
Example:
optional.BetweenTimeOpt(
OptionalTimeForm.ExpireAt,
now,
maxDate,
)Typical use cases:
- booking windows
- event scheduling
- onboarding periods
- subscription validation
Complete Example
schema.go
package main
import (
"time"
"github.com/netlifeguru/form"
"github.com/netlifeguru/form/optional"
)
const (
CodeStartAtAfter = form.Code("start_at_after")
CodeEndAtBefore = form.Code("end_at_before")
CodeExpireAtRange = form.Code("expire_at_range")
)
type OptionalTimeRequest struct {
StartAt *time.Time `json:"start_at"`
EndAt *time.Time `json:"end_at"`
ExpireAt *time.Time `json:"expire_at"`
}
func OptionalTimeSchema() form.Schema[OptionalTimeRequest] {
now := time.Now()
maxDate := now.Add(30 * 24 * time.Hour)
OptionalTimeForm := struct {
StartAt form.OptTimeField[OptionalTimeRequest]
EndAt form.OptTimeField[OptionalTimeRequest]
ExpireAt form.OptTimeField[OptionalTimeRequest]
}{
StartAt: form.OptTime[OptionalTimeRequest]("start_at", func(r *OptionalTimeRequest) *time.Time {
return r.StartAt
}),
EndAt: form.OptTime[OptionalTimeRequest]("end_at", func(r *OptionalTimeRequest) *time.Time {
return r.EndAt
}),
ExpireAt: form.OptTime[OptionalTimeRequest]("expire_at", func(r *OptionalTimeRequest) *time.Time {
return r.ExpireAt
}),
}
return form.Schema[OptionalTimeRequest]{
optional.OptionalTime(
OptionalTimeForm.StartAt,
optional.AfterOptWithCode(OptionalTimeForm.StartAt, now, CodeStartAtAfter),
),
optional.OptionalTime(
OptionalTimeForm.EndAt,
optional.BeforeOptWithCode(OptionalTimeForm.EndAt, maxDate, CodeEndAtBefore),
),
optional.OptionalTime(
OptionalTimeForm.ExpireAt,
optional.BetweenTimeOptWithCode(
OptionalTimeForm.ExpireAt,
now,
maxDate,
CodeExpireAtRange,
),
),
}
}main.go
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"time"
"github.com/netlifeguru/form"
"github.com/netlifeguru/form/httpform"
"github.com/netlifeguru/router"
)
func main() {
r := router.New()
r.HandleFunc("/optional-time", "POST", func(w http.ResponseWriter, req *http.Request, ctx *router.Context) {
var in OptionalTimeRequest
if !httpform.BindAndValidate(w, req, &in, OptionalTimeSchema(), 1<<20) {
fmt.Println("optional time validation failed")
return
}
fmt.Println("optional time validation passed:", in)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "optional time validation passed",
"data": in,
})
})
now := time.Now()
validPayload := map[string]any{
"start_at": now.Add(24 * time.Hour),
"end_at": now.Add(7 * 24 * time.Hour),
"expire_at": now.Add(3 * 24 * time.Hour),
}
invalidPayload := map[string]any{
"start_at": now.Add(-24 * time.Hour),
"end_at": now.Add(60 * 24 * time.Hour),
"expire_at": now.Add(90 * 24 * time.Hour),
}
skippedPayload := map[string]any{
"start_at": nil,
"end_at": nil,
"expire_at": nil,
}
fmt.Println("\n--- Valid request ---")
form.SendTestPost(":8080/optional-time", validPayload)
fmt.Println("\n--- Invalid request ---")
form.SendTestPost(":8080/optional-time", invalidPayload)
fmt.Println("\n--- Skipped validation request ---")
form.SendTestPost(":8080/optional-time", skippedPayload)
if err := r.ListenAndServe(8080); err != nil {
slog.Error("failed to start server", "error", err)
os.Exit(1)
}
}Validation Flow
Optional time validation behaves like this:
nil timestamp
↓
validation skippedexisting timestamp
↓
nested rules executedNotes
- Optional time validation runs only when the timestamp pointer is non-nil.
- Nil values are intentionally skipped and are not validation failures.
- Optional time validation is useful for PATCH APIs and partial updates.
- Time validation works naturally with scheduling and expiration workflows.
- Use
AfterOpt,BeforeOpt, andBetweenTimeOptfor bounded time validation. - Time validation remains reusable and transport-independent.
- Be careful when caching schemas that depend on
time.Now()directly.
Optional Float64 Validation
Learn how optional float64 validation works in NLG Form using nullable float fields, OptionalFloat64, minimum, maximum, and range validation.
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.