# Zog

> Write and review Zog schema validation in Go: schema shape, Parse against Validate, required and default semantics, tests and transforms, issue handling, and the zhttp, zjson, and zenv adapters.

- Skill: `xobotyi/zog` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add xobotyi/zog`
- Raw SKILL.md: https://api.skillmd.com/api/skills/xobotyi/zog/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: xobotyi (https://skillmd.com/u/xobotyi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/xobotyi/zog

---


Zog is a Zod-shaped API over Go semantics, and the two disagree in ways that still compile. Import as
`z "github.com/Oudwins/zog"`. Three biases decide most calls:

- **A remembered Zod default is a defect until checked.** Fields are optional by default, `z.Enum` does not exist, and a
  schema key names a Go struct field rather than an input key.
- **`Parse` and `Validate` disagree about what missing means**, and that disagreement decides which tests in a chain run
  at all.
- **Zog panics on a schema its author built wrong, and never on input data.** A panic is a defect in the schema
  definition, fixed there.

## Schema Shape

- **A `z.Shape` key names a Go struct field, never an input key.** The first letter is case-corrected, so `"name"` and
  `"Name"` both bind to `Name`. A key matching no field panics with
  `Struct Schema Definition Error ... missing expected schema key`.
- **Struct tags map input keys and nothing else.** Resolution runs `json`, `form`, `query`, or `env` for the source in
  use, then `zog`, then the schema key as written.
- **`z.Struct(...).Required()` and `.Optional()` compile and do nothing.** An optional nested struct needs
  `z.Ptr(z.Struct(...)).NotNil()`.
- **Wrap the schema in `z.Ptr` wherever the destination field is a pointer.** A `z.Slice(...)` against a `*[]T` field
  panics with a type-cast error.
- **`z.Ptr` carries only `.NotNil()`.** Put `Required`, `Default`, `Catch`, `Test`, and `Transform` on the inner schema.
- **Declare every schema once, at package level.** A schema rebuilt per call costs roughly twice the time and several
  times the memory of a reused one, and the gap widens with the number of fields.
- **`Pick`, `Omit`, `Extend`, and `Merge` return shallow copies and are not type-checked.** A key naming no struct field
  panics at execution rather than failing to compile.

Read [`${CLAUDE_SKILL_DIR}/references/schema-catalog.md`] when reaching for a constructor, a validator, a test option,
or an issue code that is not already in the chain — it carries every schema type with its full method set and
signatures, the three option families, and the issue-code and type constants.

## Missing Values

`Parse` and `Validate` implement "missing" differently, and every rule below follows from that split.

- **`Parse` treats only a nil input as missing.** `z.String().Required().Parse("", &dest)` reports nothing, and an empty
  form field, an empty query parameter, and a JSON `""` all arrive present. Add `.Min(1)` where an empty value must
  fail.
- **`Validate` treats every Go zero value as missing**, so a legitimate `0`, `""`, or `false` fails `.Required()`.
- **A missing value skips every test on an optional schema.** Under `Validate` that means `z.Bool().True()` accepts
  `false`, `z.Int().GT(10)` accepts `0`, `z.Slice(...).Min(1)` accepts an empty slice, and a custom `TestFunc` never
  runs. Wrapping in `z.Ptr(...).NotNil()` does not change it, because the inner schema still short-circuits.
- **To reject a zero value under `Validate`, use `.Required()` and set its message** —
  `z.Bool().Required(z.Message("terms must be accepted"))`. No other test reaches the value.
- **`.Required()` runs before every test and transform, whatever its position in the chain.**
  `z.String().Trim().Required()` accepts `"   "` and then trims it to `""`.
- **`.Default` outranks `.Required` and applies in both modes**, so `Validate` replaces an explicitly set zero value
  with the default. Tests still run on the substituted value.
- **`.Catch` swallows every issue** — coercion failure, required, and each test — writes its value, and stops the chain.
  Keep it off anything whose failure must reach the caller.
- **Structs support neither `.Default` nor `.Catch`; slices support `.Default` but not `.Catch`.**

## Parse and Validate

- **`Parse(data, &dest, opts...)` at an IO boundary, `Validate(&value, opts...)` on an already-typed value.** Parse
  coerces the input; Validate does not, and allocates nothing on the success path.
- **Both destinations are pointers.** A value destination panics rather than reporting an issue.
- **`z.WithCoercer` is a `SchemaOption` and belongs on the constructor** — `z.String(z.WithCoercer(fn))`.
  `z.WithCtxValue` and `z.WithIssueFormatter` are `ExecOption`s and belong on `Parse` or `Validate`.
- **Read a per-execution value inside a test with `ctx.Get(key)`**, paired with `z.WithCtxValue(key, val)` at the call.

## Tests and Transforms

- **Chain position is execution order.** `z.String().Trim().Min(3)` measures the trimmed value; `.Min(3).Trim()`
  measures the raw one.
- **`.TestFunc(fn, opts...)` is Zod's `refine`; `.Test(z.Test[T]{Func: ...})` is `superRefine`**, and the latter adds
  its own issues with `ctx.AddIssue(ctx.Issue().SetMessage(...))`.
- **A primitive test receives a typed pointer; a struct, slice, or map test receives `any`** — assert it to the struct
  pointer inside the function.
- **The package-level `z.TestFunc(code, fn, opts...)` builds a reusable `z.Test[T]` for `.Test(...)`.** It takes an
  issue code as its first argument; the method of the same name does not.
- **`.Not()` negates only the next test** and prefixes that test's issue code with `not_`. The interface it returns
  omits `Min`, `Max`, `Test`, `TestFunc`, and a second `Not`.
- **A transform mutates through its pointer and returns an error to stop the chain.** `.Trim()` is a transform, so it
  runs where it sits rather than before the tests.
- **`z.Preprocess(fn, schema)` receives the raw input under `Parse` but a pointer to the value under `Validate`.** A
  type mismatch there panics rather than reporting an issue.

## Types Without a Built-In Schema

- **A named type over a primitive needs the `Like` constructor** — `z.StringLike[Env]()`, `z.IntLike[Status]()`. A plain
  `z.String()` against a `type Env string` field panics with a type-cast error.
- **A type Zog does not know takes `z.CustomFunc(fn, opts...)`**, which validates through a typed pointer and performs
  no coercion.
- **A wrapper such as `sql.NullString` takes `z.Boxed(schema, unbox, box)`**, added in v0.21.10.

Read [`${CLAUDE_SKILL_DIR}/references/custom-schemas.md`] when a value's Go type has no built-in schema, or its input
shape does not match one — it carries the named-type, `CustomFunc`, `Boxed`, and fully custom `z.Use` forms with working
code, the `Preprocess` contract, the coercion hooks, and the experimental map and recursive constructors.

## Issues

- **Every schema returns `z.ZogIssueList`, which is `[]*z.ZogIssue`; test it with `len(errs) > 0`.**
- **An issue path segment is the input key, not the schema key.** One schema yields `first_name` through `zjson` and
  `first-name` through `Validate`, because each source resolves the field through its own tag. Give a field a `zog` tag
  equal to its `json` tag where the error keys must stay stable across both.
- **A root issue carries a nil path** and flattens under `zconst.ISSUE_KEY_ROOT`, the string `"$root"`.
- **`issue.Message` is the only field safe to return to a user.** `issue.Err` holds the underlying cause and belongs in
  a log.
- **Never read an issue after `z.Issues.Collect`** — it returns the struct to a pool the next parse draws from.

Read [`${CLAUDE_SKILL_DIR}/references/issue-handling.md`] when a validation failure has to be shaped into a response
body, a template, or a log line — it carries the `ZogIssue` fields, the path rules per input source, and the rendered
output of `Flatten`, `GroupByFlattenedPath`, `Treeify`, and `Prettify`.

## Input Adapters

- **`zhttp.Request(r)` routes on method first, then on Content-Type**, and **falls through to query parsing for any
  unrecognized content type** — a JSON body sent as `text/plain` silently yields nothing.
- **Multipart requires `r.ParseMultipartForm` in the handler first.** Without it the parser reports
  `invalid_multipart_form` and the schema never runs.
- **`zhttp` and `zjson` parse into a struct only.** A JSON array, a bare primitive, and `null` each produce one
  `invalid_json` root issue.
- **An adapter failure produces one root issue and stops** — `invalid_json`, `invalid_form`, `invalid_multipart_form`,
  or `invalid_query`.
- **`zenv` trims every value and treats an empty variable as absent**, so `PORT=` triggers `.Required()` and takes a
  `.Default()`.

Read [`${CLAUDE_SKILL_DIR}/references/input-adapters.md`] when the input comes from an HTTP request, a JSON body, or the
environment — it carries the handler and config shapes, the content-type routing table, the query and form value rules
including repeated and `[]`-suffixed keys, and the parser override hook.

## Messages

- **Set a message on the test that produces it** — `z.Message("...")`, or `z.MessageFunc` for one built from `e.Params`
  and `e.Value`.
- **Reassigning `conf.DefaultIssueMessageMap` or `conf.DefaultErrMsgMap` silently does nothing.** The formatter closes
  over the map at package initialization. Mutate entries in place, or install a new formatter with
  `conf.IssueFormatter = conf.NewDefaultFormatter(m)`.

Read [`${CLAUDE_SKILL_DIR}/references/messages-and-i18n.md`] when an issue message must be overridden or served in more
than one language — it carries the four override layers with their precedence, the message template placeholders, and
the `i18n` setup with its language-key and fallback rules.

## Panics

A panic always names a schema definition error, never bad input. Four causes account for every one:

- a `z.Shape` key naming no struct field
- a destination passed by value rather than by pointer
- a schema whose type does not match the destination field
- a coercer returning a value of the wrong Go type

Fix the schema. Never wrap a `Parse` or `Validate` call in `recover`.

## Application

When **writing** Zog, apply these conventions silently — do not narrate a rule while following it. Declare schemas as
package-level variables and check every result with `len(errs) > 0`. Where existing code contradicts a convention,
follow the codebase and flag the divergence once.

When **reviewing** Zog, cite the violation and show the fix inline. Do not lecture. Treat a `.Required()` on a value
arriving from a form, a query string, or JSON as a defect until an emptiness test sits beside it.

```
Bad:  "Required only fires on a nil input, so an empty query parameter passes through..."
Good: z.String().Required() -> z.String().Required().Min(1)
```

## Integration

The **golang** skill governs every Go decision outside the Zog API — naming, error handling, testing conventions, and
the toolchain — and wins on any question of how the Go code reads. This skill governs schema definition and the Zog
runtime API, and its key-naming rule outranks Go naming inside a `z.Shape`. The **coding** skill governs workflow. All
are active at once.

Zog is pre-1.0 and breaks its API across minor versions. Read the version in `go.mod` before writing against anything
this skill anchors to one.

**A schema states what the data must be; the mode decides what missing means.**

