| name | zog |
| description | Zog schema validation library: schema definition, parsing, validation, error handling, HTTP/JSON/env integration, custom tests, and transforms. Invoke whenever task involves any interaction with Zog — writing schemas, parsing input, validating structs, handling errors, or integrating with HTTP handlers. |
Zog
Zod-inspired schema validation for Go. Declarative schema builder with runtime parsing and validation. Import as
z "github.com/Oudwins/zog".
- All fields are optional by default (opposite of Zod)
- Schema keys in
z.Shape{} must match struct field names, not input data keys — use struct tags (json, form,
zog) for input key mapping
- All schemas return
ZogIssueList
- Check errors with
len(errs) > 0, not errs != nil
- Zog panics on schema misconfiguration (destination type mismatch, missing struct fields) but never on invalid
input data
- Do not depend on test execution order — tests may run in parallel in future versions
Parse vs Validate
Two entry points, same schemas:
schema.Parse(data, &dest, ...opts) — coerces untyped input into destination. Use at IO boundaries (HTTP, JSON, env)
schema.Validate(&value, ...opts) — validates existing Go values. No coercion. More efficient when data is already
typed
Key difference: Validate treats zero values as missing when Required() is set. Use z.Ptr() + .NotNil() to
distinguish "not provided" from "zero value":
z.Int().Required().Parse(0, &dest)
z.Ptr(z.Int()).NotNil().Validate(&valPtr)
Prefer Validate when data is already typed. Use Parse when accepting external input that needs coercion.
Schema Types
Primitives
All primitive .Parse() / .Validate() return ZogIssueList.
String:
z.String() — base string schema
.Trim() — transform: trims whitespace
.Min(n), .Max(n), .Len(n) — length validators
.Email(), .URL(), .UUID(), .IPv4() — format validators
.Match(regex) — regex match
.Contains(s), .ContainsUpper(), .ContainsDigit(), .ContainsSpecial() — content validators
.HasPrefix(s), .HasSuffix(s) — prefix/suffix validators
.OneOf([]string{...}) — enum-like validation (replaces Zod's z.Enum())
.Not() — negates the next test
Numbers:
z.Int(), z.Int32(), z.Int64(), z.Float32(), z.Float64() — numeric schemas
.GT(n), .GTE(n), .LT(n), .LTE(n), .EQ(n) — comparison validators
.OneOf([]T{...}) — enum-like validation
.Not() — negates the next test
Bool:
z.Bool() — boolean schema
.True(), .False(), .EQ(v) — value validators
Time:
z.Time() — validates time.Time
.After(t), .Before(t), .Is(t) — temporal validators
z.Time(z.Time.Format(layout)) — parse strings using layout (default: time.RFC3339). Coercion only works with
Parse(), not Validate()
Complex Types
Struct:
schema := z.Struct(z.Shape{
"name": z.String().Required(),
"age": z.Int().GT(0),
})
.Pick("key1", map[string]bool{"a": true}) — shallow copy with only specified fields
.Omit("key1", map[string]bool{"a": true}) — shallow copy without specified fields
.Extend(z.Shape{...}) — shallow copy with additional fields
.Merge(other1, other2) — merge schemas, last wins on conflict
- Structs cannot be
Required() or Optional() — use z.Ptr(z.Struct(...)) for optional structs
Slice:
schema := z.Slice(z.String().Required())
.Min(n), .Max(n), .Length(n) — size validators
.Contains(val) — element presence validator
.Not() — negates the next test
Pointer:
z.Ptr(z.String())
.NotNil() — equivalent to Required() for other types
Boxed:
z.Boxed[BoxType, InnerType](innerSchema, unboxFunc, boxFunc)
Wraps a schema with custom box/unbox logic for types like sql.NullString, driver.Valuer, or custom wrappers. The
unboxFunc extracts the inner value; boxFunc creates the wrapper from validated value (can be nil if boxing not
needed).
Custom Primitive Schemas
For named types based on primitives:
type Env string
z.StringLike[Env]().OneOf([]Env{"prod", "dev"})
Available: z.StringLike[T](), z.IntLike[T](), z.FloatLike[T](), z.UintLike[T](), z.BoolLike[T]()
CustomFunc
Quick validation for non-primitive types without defining a full schema:
z.CustomFunc(func(valPtr *uuid.UUID, ctx z.Ctx) bool {
return valPtr.IsValid()
}, z.Message("invalid uuid"))
Does not support coercion.
Generic Schema Methods
Available on all schema types:
.Required() — field must be present and non-zero
.Optional() — field can be absent (default behavior)
.Default(val) / .DefaultFunc(fn) — use this value when input is zero. Takes priority over Required(). Tests
still run
.Catch(val) / .CatchFunc(fn) — on any error, set destination to this value and stop execution
.Test(z.Test{...}) — complex custom test (like Zod's superRefine)
.TestFunc(fn, ...opts) — simple custom test (like Zod's refine)
.Transform(func(valPtr *T, ctx z.Ctx) error) — post-validation in-place mutation
Execution order: nil check → default/required → coerce → validation loop (tests + transforms in order) → catch on error
Struct Tags
Schema keys match struct field names by default. Use struct tags for input key mapping:
json — JSON input
form — form data
query — query params
env — environment variables
zog — catch-all, works for any input
Priority: json/form/query/env → zog → schema field name
type User struct {
Name string `zog:"first-name"`
LastName string `query:"last_name" json:"last-name"`
}
Custom Tests
Simple (refine-like): Return bool — Zog creates the issue:
z.String().TestFunc(func(data *string, ctx z.Ctx) bool {
return *data == "expected"
}, z.Message("must be expected"))
For structs/slices, the parameter is any — cast manually:
z.Struct(z.Shape{...}).TestFunc(func(dataPtr any, ctx z.Ctx) bool {
user := dataPtr.(*User)
return user.Name != ""
})
Complex (superRefine-like): Add issues manually via context:
z.String().Test(z.Test{
Func: func(val any, ctx z.Ctx) {
s := val.(string)
if !isValid(s) {
ctx.AddIssue(ctx.Issue().SetMessage("invalid value"))
}
},
})
Reusable tests: Wrap in functions returning z.Test[T]:
func MinWords(n int, opts ...z.TestOption) z.Test[any] {
options := []z.TestOption{z.Message(fmt.Sprintf("must have at least %d words", n))}
options = append(options, opts...)
return z.TestFunc(func(val any, ctx z.Ctx) bool {
s := val.(*string)
return len(strings.Fields(*s)) >= n
}, options...)
}
Transforms
Post-validation mutation via pointer. Runs in declaration order with tests:
z.String().Min(3).Transform(func(valPtr *string, ctx z.Ctx) error {
*valPtr = strings.ToLower(*valPtr)
return nil
})
Struct transforms receive any — cast to struct pointer:
z.Struct(z.Shape{...}).Transform(func(dataPtr any, ctx z.Ctx) error {
user := dataPtr.(*User)
user.FullName = user.First + " " + user.Last
return nil
})
Returning an error from a transform stops execution and produces an issue.
Preprocess
Transform input before parsing. Pure function — creates a copy:
z.Preprocess(func(data any, ctx z.Ctx) (any, error) {
s, ok := data.(string)
if !ok {
return nil, fmt.Errorf("expected string, got %T", data)
}
return strings.Split(s, ","), nil
}, z.Slice(z.String().Email().Required()))
Footgun: With Validate(), the data argument is a pointer to the value, not the raw input.
Error Handling
ZogIssue
type ZogIssue struct {
Code zconst.ZogIssueCode
Path []string
Value any
Dtype string
Params map[string]any
Message string
Err error
}
All schemas return ZogIssueList = []*ZogIssue. Each issue has a Path field:
- Primitive root:
nil
- Struct field:
[]string{"name"}
- Nested:
[]string{"address", "streets", "[0]"}
- Path to string:
issue.PathString() or z.Issues.FlattenPath(issue.Path)
Formatting Strategies
z.Issues.Flatten(errs) — map[path][]messages. Root issues under "$root" key (zconst.ISSUE_KEY_ROOT)
z.Issues.Treeify(errs) — nested tree mirroring data structure. Each node has errors array and properties/items
z.Issues.Prettify(errs) — human-readable string with ✖ prefix and → at path lines
Issue Codes
Common codes: required, coerce, custom, min, max, len, email, uuid, url, match, gt, gte, lt,
lte, eq, one_of_options, after, before, true, false, prefix, suffix, contains_upper,
contains_digit, contains_special, contained
HTTP-specific: invalid_json, invalid_form, invalid_query
Custom Messages
Per-test: z.Message("text"), z.MessageFunc(fn), z.IssueCode("code"), z.IssuePath("path")
z.String().Min(5, z.Message("must be at least 5 chars"))
Per-execution: z.WithIssueFormatter(fn) — overrides all messages for one Parse/Validate call
Global: Override conf.DefaultIssueMessageMap[zconst.TypeString][zconst.IssueCodeRequired] or replace
conf.IssueFormatter
Test Options
Options passed to any test:
z.Message("...") — static issue message
z.MessageFunc(func(e *ZogIssue, ctx z.Ctx)) — dynamic issue message
z.IssueCode("...") — override issue code
z.IssuePath("...") — override issue path
Execution Options
Options passed to Parse() / Validate():
z.WithCoercer(fn) — per-schema coercion override (Parse only)
z.WithIssueFormatter(fn) — override message formatting for this execution
z.WithCtxValue(key, val) — pass custom data to tests/transforms via ctx.Get(key)
Context
z.Ctx interface available in tests, transforms, and preprocessors:
ctx.Get(key) — retrieve value set via z.WithCtxValue()
ctx.AddIssue(issue) — add a custom issue
ctx.Issue() — create a new issue pre-filled with current schema context (path, value, type)
HTTP Integration (zhttp)
import "github.com/Oudwins/zog/zhttp"
errs := userSchema.Parse(zhttp.Request(r), &user)
zhttp.Request(r) auto-detects content type (JSON, form, query params). Only supports parsing into structs.
Invalid input produces a root issue with code invalid_json, invalid_form, or invalid_query — the schema does not
run.
JSON Integration (zjson)
import "github.com/Oudwins/zog/parsers/zjson"
errs := userSchema.Parse(zjson.Decode(bytes.NewReader(jsonBytes)), &user)
Accepts io.Reader or io.ReaderCloser. Only supports structs.
Environment Variables (zenv)
import "github.com/Oudwins/zog/zenv"
errs := envSchema.Parse(zenv.NewDataProvider(), &cfg)
Use env or zog struct tags for key mapping. Coercion handles string-to-type conversion automatically.
i18n
import (
"github.com/Oudwins/zog/i18n"
"github.com/Oudwins/zog/i18n/en"
"github.com/Oudwins/zog/i18n/es"
)
i18n.SetLanguagesErrsMap(map[string]i18n.LangMap{
"en": en.Map,
"es": es.Map,
}, "en")
schema.Parse(data, &dest, z.WithCtxValue(i18n.LangKey, "es"))
Single language: set conf.DefaultErrMsgMap = es.Map.
Global Configuration
Via github.com/Oudwins/zog/conf:
conf.Coercers.Float64 = func(data any) (any, error) {...} — override type coercion
conf.DefaultCoercers.Float64(data) — fallback to default coercer
conf.DefaultIssueMessageMap — override issue messages per type and code
conf.IssueFormatter — replace the global issue formatter
Patterns
Struct Validate Method
var userSchema = z.Struct(z.Shape{
"ID": z.Int().Required(),
"Name": z.String().Required().Min(2),
})
func (u *User) Validate() z.ZogIssueList {
return userSchema.Validate(u)
}
HTTP Handler
func handleCreate(w http.ResponseWriter, r *http.Request) {
var user User
if errs := userSchema.Parse(zhttp.Request(r), &user); len(errs) > 0 {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(z.Issues.Flatten(errs))
return
}
}
Environment Config
var envSchema = z.Struct(z.Shape{
"PORT": z.Int().GT(1000).LT(65535).Default(3000),
"DB_HOST": z.String().Default("localhost"),
})
var Env = func() Config {
var c Config
if errs := envSchema.Parse(zenv.NewDataProvider(), &c); len(errs) > 0 {
log.Fatal(z.Issues.Prettify(errs))
}
return c
}()
Limitations
- No
z.Map() schema
zhttp and zjson only parse into structs
- Structs/slices do not support
Catch(); structs do not support Default()
- Deeply nested schemas incur reflection overhead
Pick, Omit, Extend, Merge are not type-safe
Application
When writing code with Zog: apply all conventions silently — don't narrate each rule. Always check errors with
len(errs) > 0. Define schemas as package-level var, not inside functions. If an existing codebase contradicts a
convention, follow the codebase and flag the divergence once.
When reviewing code with Zog: cite the specific violation and show the fix inline. Proactively flag: errs != nil
checks (must be len(errs) > 0), missing Required() on mandatory fields, schema keys not matching struct field names,
and missing struct tags for external input mapping.
Integration
The golang skill governs Go language conventions. This skill governs Zog-specific API and patterns. When both apply,
Zog idioms (e.g., z.Shape{} key naming) take precedence over general Go naming rules within schema definitions.