Imported from xobotyi/cc-foundry (
plugins/golang/skills/zog/SKILL.md). Install upstream withnpx skills add xobotyi/cc-foundry --skill zog. Copyright stays with the author.
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.Enumdoes not exist, and a schema key names a Go struct field rather than an input key. ParseandValidatedisagree 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.Shapekey names a Go struct field, never an input key. The first letter is case-corrected, so"name"and"Name"both bind toName. A key matching no field panics withStruct Schema Definition Error ... missing expected schema key. - Struct tags map input keys and nothing else. Resolution runs
json,form,query, orenvfor the source in use, thenzog, then the schema key as written. z.Struct(...).Required()and.Optional()compile and do nothing. An optional nested struct needsz.Ptr(z.Struct(...)).NotNil().- Wrap the schema in
z.Ptrwherever the destination field is a pointer. Az.Slice(...)against a*[]Tfield panics with a type-cast error. z.Ptrcarries only.NotNil(). PutRequired,Default,Catch,Test, andTransformon 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, andMergereturn 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.
Parsetreats 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.Validatetreats every Go zero value as missing, so a legitimate0,"", orfalsefails.Required().- A missing value skips every test on an optional schema. Under
Validatethat meansz.Bool().True()acceptsfalse,z.Int().GT(10)accepts0,z.Slice(...).Min(1)accepts an empty slice, and a customTestFuncnever runs. Wrapping inz.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""..Defaultoutranks.Requiredand applies in both modes, soValidatereplaces an explicitly set zero value with the default. Tests still run on the substituted value..Catchswallows 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
.Defaultnor.Catch; slices support.Defaultbut 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.WithCoerceris aSchemaOptionand belongs on the constructor —z.String(z.WithCoercer(fn)).z.WithCtxValueandz.WithIssueFormatterareExecOptions and belong onParseorValidate.- Read a per-execution value inside a test with
ctx.Get(key), paired withz.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'srefine;.Test(z.Test[T]{Func: ...})issuperRefine, and the latter adds its own issues withctx.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 reusablez.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 withnot_. The interface it returns omitsMin,Max,Test,TestFunc, and a secondNot.- 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 underParsebut a pointer to the value underValidate. A type mismatch there panics rather than reporting an issue.
Types Without a Built-In Schema
- A named type over a primitive needs the
Likeconstructor —z.StringLike[Env](),z.IntLike[Status](). A plainz.String()against atype Env stringfield 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.NullStringtakesz.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 withlen(errs) > 0. - An issue path segment is the input key, not the schema key. One schema yields
first_namethroughzjsonandfirst-namethroughValidate, because each source resolves the field through its own tag. Give a field azogtag equal to itsjsontag 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.Messageis the only field safe to return to a user.issue.Errholds 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 astext/plainsilently yields nothing.- Multipart requires
r.ParseMultipartFormin the handler first. Without it the parser reportsinvalid_multipart_formand the schema never runs. zhttpandzjsonparse into a struct only. A JSON array, a bare primitive, andnulleach produce oneinvalid_jsonroot issue.- An adapter failure produces one root issue and stops —
invalid_json,invalid_form,invalid_multipart_form, orinvalid_query. zenvtrims every value and treats an empty variable as absent, soPORT=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("..."), orz.MessageFuncfor one built frome.Paramsande.Value. - Reassigning
conf.DefaultIssueMessageMaporconf.DefaultErrMsgMapsilently does nothing. The formatter closes over the map at package initialization. Mutate entries in place, or install a new formatter withconf.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.Shapekey 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.
