README
¶
go-z
Schema-first validation for Go. Define a schema once, parse anything into it, and get structured errors back — with the ergonomics of a fluent schema API and the performance of hand-written Go.
Inspired by Zod, not affiliated with it. go-z is an independent project, not endorsed by or sponsored by Zod or its authors. Portions are derived from Zod under the MIT licence — see NOTICE.
Documentation · Quickstart · API reference · Benchmarks · Changelog
Install
go get github.com/iKunalChhabra/go-z/z
Requires Go 1.26+.
The core package has a single dependency, golang.org/x/text,
used for Unicode NFC normalisation in Normalize(). Nothing else enters your
go.sum. The Gin integration is a separate module, so
Gin is only downloaded if you ask for it:
go get github.com/iKunalChhabra/go-z/zgin
Quick start
package main
import (
"fmt"
"github.com/iKunalChhabra/go-z/z"
)
func main() {
user := z.Object(z.Shape{
"name": z.String().Min(2).Max(100),
"email": z.String().Email(),
"age": z.Int().Gte(0).Lt(150).Optional(),
})
data, err := user.Parse(map[string]any{
"name": "Ada",
"email": "ada@example.com",
})
if err != nil {
zerr, _ := z.AsError(err)
fmt.Println(z.Prettify(zerr))
return
}
fmt.Println(data) // map[email:ada@example.com name:Ada]
}
The package is named z and lives at /z, so a plain import already reads the
way you would want — no alias required:
import "github.com/iKunalChhabra/go-z/z"
z.String().Min(5).Email()
Features
- Fluent schema API.
z.String().Min(5).Email(),z.Object(z.Shape{…}),Optional/Nullable/Default/Catch/Pipe/Transform/Refine. - Typed edges.
String().Optional().Parse(v)returns(*string, error);String.Default("x").Parse(v)returns(string, error). Generic wrappers keep the inner type instead of collapsing toany. - Structured errors. Eleven issue codes with paths and stable JSON field
names, plus
Flatten/Format/Treeify/Prettify. - Bidirectional codecs.
z.Decode/z.Encodewith direction-aware defaults. - JSON Schema export.
z.ToJSONSchemafor OpenAPI and client-side validators. - i18n. Error maps and seven locales (
en es fr de ja pt zh). - Concurrency-safe. Schemas are immutable after construction;
Parseis lock-free and-raceclean.ParseParallelSlicefans large slices across cores. - Gin integration.
zgin.Validate,zgin.BindJSON, typedzgin.GetAs[T].
Usage
Safe parsing
res := z.String().Email().SafeParse("nope")
if !res.Success {
fmt.Println(res.Error.Issues[0].Code) // invalid_format
}
Objects, unions, recursion
var Category z.AnySchemaLike
Category = z.Lazy(func() z.AnySchemaLike {
return z.Object(z.Shape{
"name": z.String().Min(1),
"children": z.Array(Category).Default([]any{}),
})
})
userOrGuest := z.DiscriminatedUnion("role", []z.AnySchemaLike{
z.Object(z.Shape{"role": z.Literal("admin"), "perms": z.Array(z.String())}),
z.Object(z.Shape{"role": z.Literal("guest"), "session": z.String().UUID()}),
})
Structs
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
parsed, err := z.ToStruct[User](user).Parse(input) // parsed is a User
Codecs
isoDate := z.Codec(z.String().ISODateTime(), z.Time(), z.CodecTx{
Decode: func(v any, _ *z.RefinementCtx) (any, error) {
return time.Parse(time.RFC3339Nano, v.(string))
},
Encode: func(v any, _ *z.RefinementCtx) (any, error) {
return v.(time.Time).UTC().Format(time.RFC3339Nano), nil
},
})
t, _ := z.Decode(isoDate, "2024-01-15T10:30:00Z") // time.Time
s, _ := z.Encode(isoDate, t) // ISO string
Gin
import "github.com/iKunalChhabra/go-z/zgin"
r.POST("/users", zgin.Validate(user), func(c *gin.Context) {
body, _ := zgin.Get(c) // already parsed and validated
c.JSON(200, body)
})
Failed validation writes structured issues automatically:
{"success":false,"error":{"issues":[{"code":"too_small","path":["name"],"message":"Too small: expected string to have >=2 characters"}]}}
Flatten, Treeify, and Prettify renderers are available via zgin.Options.
Concurrency
schema := z.String().Email() // build once, share freely
go func() { schema.Parse(a) }()
go func() { schema.Parse(b) }()
out, err := z.ParseParallelSlice(ctx, itemSchema, items, z.ParallelOpts{})
Performance
4-core Xeon, Go 1.26.5, median of nine runs on a shared cloud VM. Full methodology, spread, and tables in BENCHMARKS.md.
| Scenario | go-z | go-playground/validator | Oudwins/zog |
|---|---|---|---|
| Flat object | 528 ns | 637 ns | 1258 ns |
| Nested object | 1184 ns | 1112 ns | 2646 ns |
| String formats (email + uuid + url) | 798 ns | 1088 ns | 1713 ns |
| Array of 10k (parallel) | 2.45 ms | 6.28 ms | 12.5 ms |
Email, UUID, and the ISO date/time formats use hand-written matchers rather than backtracking regexes; each is differential-tested against the regex it replaced over hundreds of thousands of random inputs. Two places go-z does not win: nested objects, where it is ~6% behind validator because every level costs a child payload and a path segment, and the failure path, which is ~3.6× slower because structured issues are finalized through the error-map chain.
Design notes
- Untyped core, typed edge. The engine runs on
any;Schema[T]is the generic boundary.OptionalandNullableyield*T(nil means absent or null); every other wrapper yieldsT. Each wrapper has a type-erased constructor for heterogeneous containers (Optional(anySchema)) and a typed one (OptionalOf,DefaultOf,RefineOf, …) that works with every schema type. - JSON model first. Objects produce
map[string]anyand arrays[]any; useToStruct[T]when you want a struct. Numeric schemas produce the Go type they are named after —Intanint,Uint32auint32,Numberafloat64— converting the incoming JSON number only when the conversion is exact. Missingis notnil.Missingmeans an absent key (JSundefined);nilis JSONnull.Optionalaccepts Missing,Nullableaccepts nil,Nullishboth.- Params are checked at definition time. An unsupported params type panics while the schema is built — at startup, never during request handling.
- Object field order.
Object(Shape)reports issues in sorted key order because Go maps are unordered;ObjectOrdered([]Field{…})preserves definition order.
Project layout
Every package is a directory; none of them is special.
z/ core package — import "github.com/iKunalChhabra/go-z/z"
schema_*.go schema types (string, number, object, union, codec, …)
checks_*.go composable checks
fluent*.go mid-chain Optional/Default/Refine on concrete schemas
matchers.go hand-written format matchers
errorutils.go Flatten / Format / Treeify / Prettify
jsonschema.go ToJSONSchema
locale_*.go i18n error maps
parallel.go ParseParallelSlice
tostruct.go cached reflect decode
zgin/ Gin binding and middleware (separate module)
bench/ comparative benchmarks (separate module)
docs/ documentation site
Status
Implemented: primitives and string formats, objects and collections, unions / xor /
discriminated unions / intersection / lazy, wrappers, codecs, ToJSONSchema,
template literals, coercion, error utilities, seven locales, Gin, struct binding,
and parallel parsing. Behavioural parity is tracked in z/parity_*_test.go — see
PARITY.md.
Not implemented: fromJSONSchema, and the JavaScript-only surface (z.function(),
z.promise(), z.symbol(), z.file()). Async parsing is unnecessary in Go.
Contributing
Issues and pull requests are welcome. Before submitting:
go test -race ./...
go vet ./...
gofmt -l .
Author
Kunal Chhabra (@iKunalChhabra)
Licence and attribution
MIT — Copyright (c) 2026 Kunal Chhabra. See LICENSE.
Portions of this project are derived from Zod (MIT, Copyright (c) 2025 Colin McDonnell): string-format patterns, locale message text, the issue taxonomy, and behavioural test cases ported from Zod's test suite. Zod's licence is reproduced in full in NOTICE.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package z provides schema-first validation for Go: schemas are values you build, compose and share, and parsing returns a typed result or a structured error describing every failure.
|
Package z provides schema-first validation for Go: schemas are values you build, compose and share, and parsing returns a typed result or a structured error describing every failure. |
|
zgin
module
|