Zog: a Zod-style schema validator for Go, with a caveat about its own API
Go with Zod inspired simple schema validation
At a glance
- What is it?
- Zog builds runtime validation schemas in Go with a chained, Zod-like API, ships helper packages for HTTP, JSON, environment variables and i18n errors, and has zero dependencies, but the author warns that minor version bumps can break your code.
- Who is it for?
- Zog fits a Go team that wants schema-driven parsing with readable errors and the same ergonomics a TypeScript team gets from Zod, especially one that validates HTTP input, JSON and environment variables through one schema definition. It is a poor fit if you need a stable API across minor versions, because the author states plainly that Zog is still in version 0 and will have breaking changes in the minor versions as per semver.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 54 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 15, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library is for
Zog is a schema builder for runtime value parsing and validation. You define a schema, then transform a value to match it, assert the shape of an existing value, or both, and the schemas are expressive enough to model complex, interdependent validations or value transformations.
The design lineage is explicit. The title calls it a Zod and Yup like schema parser and validator for Go, the API is described as Zod-like with method chaining to build schemas in a typesafe manner, and the acknowledgements give credit to colinhacks/zod and jquense/yup for the inspiration. The initial idea is credited to anthony and to his superkit project, described as a hacky version of the same idea that the author used as a starting point and was never happy with.
The difference from the Go libraries that already existed is worth stating. A struct-tag based validator asks you to describe rules in tags next to the struct. Zog asks you to build the schema as a value, which means the rules are ordinary Go code, they can be composed, and they can be passed around.
The feature list calls out concise yet expressive schema interface, extensibility with your own tests and schemas, rich errors with detailed context, built-in coercion support for most types, and zero dependencies. The speed claim is comparative and qualified: Zog is one of the fastest Go validation libraries, just behind goplayground/validator for most of the govalidbench benchmarks, and the benchmark suite itself lives in a separate repository under the author's name.
The project is MIT licensed, the module is github.com/Oudwins/zog, the documentation is at zog.dev, and the last push was on 2026-08-30 with v0.23.0 released the same day.
The stability caveat the author puts at the top
Before anything else, the README carries an API Stability note, and it is unusually blunt for a library that wants adoption.
The author says he will consider the API stable when the project reaches v1.0.0. He also says he believes very little API change will happen from the current implementation, and names the exceptions: the data providers, which he asks you not to build yourself if you can use the helpers whose APIs will not change meaningfully, and the z.Ctx, with other APIs expected to remain the same.
Then the sentence that should govern your dependency policy: although we want to keep breaking changes to a minimum, Zog is still in version 0 and will have breaking changes in the minor versions as per semver. So please be careful when upgrading minor versions.
That is a candid contract, and it is better than discovering it during an upgrade. A minor version bump is the semver signal for additive change, so a library that breaks in minors is using the version number against its usual meaning, and a team with a lockfile and a habit of pinned upgrades will be fine while a team that runs go get -u will not.
The practical advice follows from the note. The data providers are the moving part, so prefer the helper packages over hand-rolled providers, because the helpers are the ones the author commits to keeping stable. And the fact that the project is at 0.x with a v1.0.0 target means the API freeze is something still in the future, not something you can rely on today.
The release history is consistent with that: v0.23.0 on 2026-08-30, v0.22.2 on 2026-04-25, v0.22.1 on 2026-04-18. Minor versions are moving quickly.
Four helper packages, and one agent skill
The helper packages are the reason to pick Zog over a plain validator, because they remove the part everyone writes badly.
zenv parses environment variables. The usage is a data provider passed to Parse:
import (
zenv "github.com/Oudwins/zog/zenv"
)
err := envSchema.Parse(zenv.NewDataProvider(), &envs)zhttp parses HTTP forms and query parameters, and it also handles JSON, so one schema can serve a request regardless of how the body arrived:
import (
zhttp "github.com/Oudwins/zog/zhttp"
)
err := userSchema.Parse(zhttp.Request(r), &user)If the JSON arrives some other way, zjson decodes it directly:
import (
zjson "github.com/Oudwins/zog/zjson"
)
err := userSchema.Parse(zjson.Decode(bytes.NewReader(jsonBytes)), &user)And the fourth is i18n, described as an opinionated solution to good i18n zog errors, which is the part that turns a validation failure into a message a user in another language can read.
There is also an agent skill, which the project describes as a way for you to write as little boilerplate as possible yourself, with usage guidance, common mistakes and best practices. It installs with npx skills add https://github.com/Oudwins/zog, or you can copy it from the skills directory, and the repository has a .agents directory at the root.
The dependency picture is small. The go.mod requires github.com/google/jsonschema-go at 0.4.3, stretchr/testify at 1.9.0 for the tests, and golang.org/x/exp, with the module targeting Go 1.23.0. The README's zero dependencies claim refers to the runtime path, and the test and schema tooling pull in the rest.
Parse versus Validate, and the empty string that is not an error
There are two entry points and the difference matters, because it determines whether your schema describes a transformation or an assertion.
Parse transforms a value to match the schema, and it is what you use when the input is untrusted:
func main() {
u := User{}
m := map[string]string{
"name": "Zog",
"age": "", // won't return an error because fields are optional by default
}
errs := userSchema.Parse(m, &u)
if errs != nil {
// handle errors -> see Errors section
}
u.Name // "Zog"
u.Age // 0
}Validate asserts the shape of a value that already exists:
func main() {
u := User{
Name: "Zog",
Age: 0, // wont return an error because fields are optional by default otherwise it will error
}
errs := userSchema.Validate(&u)
if errs != nil {
// handle errors -> see Errors section
}
}The comment on the age field is the thing to internalise: fields are optional by default, so an empty string is skipped rather than reported, and you are left with the uninitialised int. The documented fix is to use a pointer to an int, which will be nil if the value was not present in the input data. If zero is a legitimate value for a field, a pointer is the only way to tell absent from zero, and that is a decision you have to make when you define the struct rather than when the error appears.
There is also a comment about the schema keys that is easy to miss and expensive to get wrong. The schema keys like name must match the struct field name, not the input data, so a JSON payload with a different key needs a different schema key or a transformation.
Individual fields can be parsed on their own too, and the return type is uniform:
var t = time.Time
// All schemas now return ZogIssueList
errs := Time().Required().Parse("2020-01-01T00:00:00Z", &t)Preprocess, and the roadmap that is not done yet
The transformation side goes further than coercion, through a Preprocess function that runs before the schema does its work:
var dest []string
schema := z.Preprocess(func(data any, ctx z.Ctx) ([]string, error) {
s := data.(string) // don't do this, actually check the type
return strings.Split(s, ","), nil
}, z.Slice(z.String().Trim().Email().Required()))
errs := schema.Parse("[email protected],[email protected]", &dest) // dest = [[email protected] [email protected]]So a comma separated string becomes a slice of validated, trimmed, email-checked strings. The comment in the sample is the author's own advice, check the type rather than asserting it, and the signature returns an error, so a preprocess step that cannot handle the input has a way to say so.
The roadmap names three things wanted before v1.0.0: support for schema.Clone(), support for catch and default for structs and slices, and struct generation from the schemas.
That third one is the interesting one for an adopter, because generating a Go struct from a schema would close the loop the library currently leaves open. Today the struct and the schema are written separately and kept in sync by hand, and the example in the README shows both in the same file with a comment about the keys having to match. Struct generation would remove that class of bug, and until it exists, the sync is your problem.
The schema.Clone() item is what a library like this needs to compose schemas, and catch and default is the standard validation feature for supplying a fallback value when the input is absent. Both are on the list, so a project that needs them today has to work around them.
The repository itself reflects a library in active development. The root is a flat list of source files, any.go, boolean.go, boxedSchema.go, custom.go, maps.go, numbers.go, pointers.go, preprocess.go and recursive.go, with a test file next to each, plus a parsers directory, a pkgs directory for the helper packages, an i18n directory, a cmd directory holding the schema generator, and a docs directory with its own npm setup. The Makefile has the usual targets, test and test-watch through gotestsum, a test-cover target that runs with the race detector and atomic cover mode and then filters out the lines already at 100 percent, a lint target pinned to golangci-lint v1.63.4, and a schema-gen target that runs cmd/zssschema-gen/main.go with a version argument.
Editorial conclusion
Zog fits a Go team that wants schema-driven parsing with readable errors and the same ergonomics a TypeScript team gets from Zod, especially one that validates HTTP input, JSON and environment variables through one schema definition. It is a poor fit if you need a stable API across minor versions, because the author states plainly that Zog is still in version 0 and will have breaking changes in the minor versions as per semver. Before adopting, read the API stability note in the README, and if your values can legitimately be zero or empty, use a pointer type in your struct, because a missing field is skipped rather than reported as an error.
Frequently asked questions
What is Zog and what does it do?
Zog is a schema builder for runtime value parsing and validation in Go. You define a schema, then transform a value to match it, assert the shape of an existing value, or both, with a chained Zod-like API and schemas expressive enough to model interdependent validations.
How do I install Zog and define a schema?
Run go get github.com/Oudwins/zog, then build the schema with z.Struct(z.Shape{...}) mapping schema keys to struct field names, using z.String().Min(...).Max(...) or z.Int().GT(...) for the field rules. The schema keys must match the struct field names, not the input data.
What is the difference between Zog Parse and Validate?
Parse transforms a value to match the schema and is for untrusted input, while Validate asserts the shape of a value that already exists. All schemas return a ZogIssueList, so field-level parsing returns the same error type as a struct parse.
Why does my Zog validation not complain about an empty field?
Fields are optional by default, so an empty string is skipped rather than reported and you are left with the uninitialised value. If you need zero to be a valid value, use a pointer to an int, which will be nil when the value was not present in the input data.
Is the Zog API stable?
Not yet. The author states that he will consider the API stable at v1.0.0, that Zog is still in version 0 and will have breaking changes in the minor versions as per semver, and that the data providers and z.Ctx are the APIs most likely to change, so you should prefer the helper packages over writing your own providers.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/oudwins-zog)