NetLife Guru

Open source Go packages for fast, maintainable web systems. Built with a documentation-first approach.

Product
OverviewGolang packagesNews
Documentation
DocumentationGo LoggerGo RouterGo DB Form
Company
OverviewContactNewsGitHub
Community / Support
Supportinfo@netlife.guru
© 2026 NetLife Guru. All rights reserved.
GitHubinfo@netlife.guru
NetLife GuruNetLife GuruNetLife Guru
NetLife GuruNetLife GuruNetLife Guru
OverviewDocumentationNewsSupportContact

Golang packages

About
Optional ValidationOptional String ValidationOptional Integer ValidationOptional Float64 ValidationOptional Time ValidationOptional Pointer ValidationOptional Slice Validation
Form ValidatorOptional Validation

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

HelperDescription
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:

InputResult
nullskipped
omitted fieldskipped
valid timestampvalidated
invalid rangevalidation 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 skipped
existing timestamp
    ↓
nested rules executed

Notes

  • 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, and BetweenTimeOpt for 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.

On this page

Why OptionalTime ExistsTL;DRDefining Optional Time FieldsApplying Optional Time RulesAfterOptBeforeOptBetweenTimeOptComplete Exampleschema.gomain.goValidation FlowNotes