Introduction to TypeScript and Go
TypeScript and Go now occupy a prominent place in the minds of contemporary engineers. The former grafts optional, static typing onto JavaScript's freewheeling syntax, thereby easing the perennial pain of runtime errors. The latter pares programming down to its essentials, affording near-aphasic developers performance and effortless concurrency.
Lately I've been flitting between both languages on separate projects, and the split in their validation cultures jumps out immediately. TypeScript contributors lean on expressive, DSL-like utilities such as Zod that reduce schema verification to a handful of fluent method calls. Go teams, by contrast, often revert to reflection-centred code that is both bulky and, in some edge cases, a noticeable drain on execution speed.
That asymmetry gnawed at me until it posed an almost gluttonous challenge: Why couldn't Gopher's ecosystem borrow at least a slice of TypeScript's validation magic? Would it be possible to import its clean syntax and ergonomic chainability without smuggling in the heaviness that usually tags along with transpiled JavaScript?
Understanding the Need for a Validation Library
Every production application, regardless of stack, must grapple with the drudgery of data validation. User forms, API payloads, even the sporadic upstream feeds from third-party services demand a guarantee that incoming structures match the formats we expect. When that guarantee is absent, bugs slip through, lose the light of day, and often show up at the worst imaginable moment.
In short, prolonged confidence in data hygiene is not a luxury; it is the unobtrusive backbone that allows software to change confidently. Each time an assumption goes unverified, the cost compounds until fixing it becomes harder than building the feature in the first place. Thus a tidy, readable validation layer is less an optional ornament than an embedded organ of any well-mannered production codebase.
Validation in Go often feels sluggish because the standard routines seem stuck in quicksand.
In a typical project, a developer might write 50 lines of ad-hoc code just to check a user struct.
// The old way - verbose, error-prone, no reusability
if user.Name == "" {
return errors.New("name is required")
}
if len(user.Name) < 3 {
return errors.New("name must be at least 3 characters")
}
if len(user.Email) == 0 {
return errors.New("email is required")
}
// ... 50 more lines of repetitive validation
None of those blocks—require non-empty string, enforce minimum length, repeat—errors reusable or neat.
Reflection-Based Tools
Many existing libraries lean on reflection, which makes the checks slower than hand-rolled loops.
When speed matters, that penalty is more than a micro-benchmark—frequent tests drag the entire CI.
Error messages from these libraries tend to land as something like field_name: invalid
without context.
Fluent chaining, another popular request, is absent or half-baked in most canon packages.
A New Benchmark
Programmers have been asking for a validator that pairs raw speed with decent developer love.
Some even hint at TypeScript's Zod when they sketch wish-lists on Slack.
Inspiration from Zod: What Makes it a Popular Choice in TypeScript?
What Zod Gets Right
A fluent, chainable API lets you read a schema almost like a sentence.
const userSchema = z.object({
name: z.string().min(3).max(50),
email: z.string().email(),
age: z.number().min(18).max(120)
});
Errors arrive in structured JSON, so front ends can parse them but so can log analyzers.
TypeScript's type system quietly inhales every Zod shape, giving compile-time safety all the way to the network.
Small, self-contained code means zero hidden dependencies that bloated the bundle.
Zod schemas stack neatly, letting a password rule reuse the email rule without copy-paste hell.
Those guiding ideas eventually coalesced into a validation library the team called zod-go.
The Challenges of Translating TypeScript Principles to Go
Moving Zod's distinctive design across languages exposed several friction points.
First, the two type systems do not speak the same grammatical rule book. Go is statically typed yet has only minimal inference compared to TypeScript. Crafting an expressive API while locking down the type surface took painful care.
Performance quickly surfaced as another non-negotiable. Most Go users expect libraries to hover near native speed; anything slower griefed their error log.
Flexibility in Go arrives via interface{}
but eats compile-time checks for breakfast. Balancing runtime liberty with compile-time confidence became a constant tug-of-war.
Method chaining, an idiomatic TypeScript flourish, sits awkwardly in Go. Each call must return the precise interface if fluent syntax is to survive.
Concurrency sits at the heart of the Go ethos. To audit large slices without blocking the universe, the design had to embrace goroutines without losing clarity.
Three months of headaches later a prototype landed that neither crawled nor wrote spaghetti. zod-go passed the benchmarks and still felt light in the hands.
Pleasingly, syntax remained close to the original Zod.
schema := validators.String().
Min(8).
Pattern(`[A-Z]`).
WithMessage("pattern", "Must contain uppercase").
Required()
The Creation of zod-go: A Performance-driven Validation Library
Overview of Key Features and Functionalities
Rich Data Type Support:
A single library handles multiple formats: strings accept regular-expression matching plus length and format guards, numbers come with precise range and type limits, boolean fields can be marked mandatory, and array entries undergo element checks while enforcing uniqueness. Nested objects and maps are each governed by their own schema rules.
Detailed Error Reporting:
The core validation error type packages useful metadata:
type ValidationError struct {
Field string `json:"field"`
Value interface{} `json:"value"`
Message string `json:"message"`
Rule string `json:"rule"`
Details []ErrorDetail `json:"details,omitempty"`
}
Concurrent Processing:
Heavy workloads benefit from parallelism:
results := zod.ValidateConcurrently(schemas, dataList, 4) // launches four goroutines and aggregates outcomes
Custom Validator Support:
Special cases travel via plug-in functions. An email-domain check looks like this:
if !strings.HasSuffix(email, "@company.com") {
return zod.NewValidationError("domain", email, "must be company email")
}
Performance Benchmarks and Improvements Compared to Existing Solutions
Results from my test suite arrived almost shockingly fast. A simple string test hit 50 million calls in 32.1 nanoseconds per op with no allocations.
Comparison Numbers:
BenchmarkZodGo_SimpleString-8 50000000 32.1 ns/op 0 B/op
BenchmarkGoPlayground_SimpleString-8 5000000 385.2 ns/op 64 B/op
BenchmarkGoValidator_SimpleString-8 3000000 441.8 ns/op 96 B/op
BenchmarkZodGo_ComplexObject-8 1000000 1247.3 ns/op 128 B/op
BenchmarkGoPlayground_ComplexObject-8 100000 12847.1 ns/op 2048 B/op
Key Performance Achievements:
Zod now outpaces well-known reflection validators by at least 10 to 12 times. Routine checks incur zero heap churn, memory pressure eases via pooling, and concurrent jobs scale almost linearly with CPU cores.
Several optimizations drove the latest performance boosts:
- Direct type assertions supplanted the reflective calls once used.
- Error structs now borrow from an object pool, reducing heap churn.
- A worker pool pattern handles concurrent validation in tidy batches.
- Memory allocation paths have been pruned and trimmed for lower latency.
How TypeScript Principles were Incorporated into zod-go
Zod-go channels TypeScript ideals even within Go's statically typed world.
Type Safety and Schema Validation
Go lacks the deep compile-time inference that TypeScript fans celebrate, yet zod-go secures type safety by confining methods to narrow interfaces. For instance, the StringValidator
interface exposes only those functions meaningful for string checks.
type StringValidator interface {
Schema
Min(int) StringValidator
Max(int) StringValidator
Pattern(string) StringValidator
Email() StringValidator
Required() StringValidator
WithMessage(rule, message string) StringValidator
}
Individual validators guard themselves, preventing accidental calls to length methods on an email-checking context.
Schema Composition
Road address formats nest cleanly inside larger structures:
addressSchema := validators.Object(map[string]zod.Schema{
"street": validators.String().Required(),
"city": validators.String().Required(),
"zipCode": validators.String().Pattern(`^\d{5}$`),
})
User records build atop that blueprint:
userSchema := validators.Object(map[string]zod.Schema{
"name": validators.String().Min(2).Required(),
"email": validators.String().Email().Required(),
"address": addressSchema,
})
Error Handling and Custom Error Messages
When validation stalls, zod-go packages feedback in a hierarchical tree rather than a flat list.
Custom Error Messages
Password rules show the structure at work:
schema := validators.String().
Min(8).WithMessage("minLength", "Password must be at least 8 characters").
Pattern(`[A-Z]`).WithMessage("pattern", "an uppercase letter is mandatory in the password field")
Developers receive both the policy name and its user-friendly string.
Structured Error Output
{
"field": "user.address.zipCode",
"value": "invalid-zip",
"message": "must match pattern ^\\d{5}$",
"rule": "pattern",
"details": [
{
"field": "zipCode",
"message": "the provided ZIP code does not conform to the expected format"
}
]
}
Error Aggregation
In contrast to some libraries that halt validation upon the first failure, zod-go accumulates every encountered error. This all-at-once reporting is invaluable for diagnosing input problems.
Extensibility and Modularity
Custom Validators
type CustomValidator func(data interface{}) error
func isValidProductCode(data interface{}) error {
code, ok := data.(string)
if !ok {
return zod.NewValidationError("type", data, "input must be of type string")
}
if !regexp.MustCompile(`^[A-Z]{2}\d{6}$`).MatchString(code) {
return zod.NewValidationError("format", code, "product code must follow the TWOLETTERSYYYYYY pattern")
}
return nil
}
schema := validators.String().Custom(isValidProductCode)
Middleware Support
// Prepend a logging layer to track which fields are checked
loggingValidator := WithLogging(userSchema)
// Use an in-memory cache to avoid re-validating expensive computations for five minutes
cachedValidator := WithCache(complexSchema, 5*time.Minute)
Real-life Use Cases: Examples of zod-go in Action
Validating User Input in Web Applications
User Registration Endpoint
type RegistrationRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Profile struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Bio string `json:"bio"`
} `json:"profile"`
}
var registrationSchema = validators.Object(map[string]zod.Schema{
"username": validators.String().
Min(3).Max(20).
Pattern(`^[a-zA-Z0-9_]+$`).
WithMessage("pattern", "username may only consist of alphanumeric characters and underscores").
Required(),
"email": validators.String().
Email().
WithMessage("email", "the supplied email address appears to be invalid").
Required(),
"password": validators.String().
Min(8).
Pattern(`[A-Z]`).WithMessage("uppercase", "An uppercase letter is required").
Pattern(`[a-z]`).WithMessage("lowercase", "At least one lowercase letter is needed").
Pattern(`\d`).WithMessage("digit", "Include a digit for security").
Required(),
"profile": validators.Object(map[string]zod.Schema{
"firstName": validators.String().Min(1).Max(50).Required(),
"lastName": validators.String().Min(1).Max(50).Required(),
"bio": validators.String().Max(500).Optional(),
}).Required(),
})
func handleRegistration(w http.ResponseWriter, r *http.Request) {
var req RegistrationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := registrationSchema.Validate(req); err != nil {
if validationErr, ok := err.(*zod.ValidationError); ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(validationErr.ErrorJSON()))
return
}
http.Error(w, "Validation failed", http.StatusBadRequest)
return
}
// Process valid registration...
}
Data Validation in API Requests
Governance of Product Data
var productSchema = validators.Object(map[string]zod.Schema{
"name": validators.String().Min(1).Max(200).Required(),
"description": validators.String().Min(10).Max(2000).Required(),
"price": validators.Number().Min(0.01).Required(),
"category": validators.String().OneOf([]string{"electronics", "clothing", "books", "home", "sports"}).Required(),
"tags": validators.Array(validators.String().Min(1).Max(50)).Min(1).Max(10).Unique(),
"dimensions": validators.Object(map[string]zod.Schema{
"length": validators.Number().Min(0),
"width": validators.Number().Min(0),
"height": validators.Number().Min(0),
"weight": validators.Number().Min(0),
}),
"inStock": validators.Bool().Required(),
})
func createProduct(w http.ResponseWriter, r *http.Request) {
var product map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&product); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := productSchema.Validate(product); err != nil {
if validationErr, ok := err.(*zod.ValidationError); ok {
response := map[string]interface{}{
"error": "Validation failed",
"details": validationErr,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(response)
return
}
http.Error(w, "Validation failed", http.StatusBadRequest)
return
}
// Create product in database...
}
Bulk Data Processing
The following function accepts a slice of arbitrary user records and assembles a schema-per-record blueprint:
func processBulkUsers(users []interface{}) []Result {
schemas := make([]zod.Schema, len(users))
for i := range schemas {
schemas[i] = userSchema
}
// Processing thousands of records simultaneously is accomplished via
// zod.ValidateConcurrently and a concurrency limit of eight workers.
results := zod.ValidateConcurrently(schemas, users, 8)
validUsers := make([]interface{}, 0)
errors := make([]zod.ValidationError, 0)
for i, result := range results {
if result.IsValid {
validUsers = append(validUsers, users[i])
} else {
errors = append(errors, *result.Error.(*zod.ValidationError))
}
}
return results
}
A compact slice validUsers
captures entries that pass all checks, while another slice errors
collects the specific validation failures encountered along the way.
The function finally surfaces a set of Result objects that indicate the overall validation outcome for each initial user entry.
Future Development and Roadmap
The collaboration with zod-go is in its infancy, and several significant enhancements are on the horizon.
Schema Inference from Go Structs
Future releases will support automatic schema creation directly from struct tags, allowing a definition such as:
type User struct {
Name string `validate:"min=3,max=50,required"`
Email string `validate:"email,required"`
Age int `validate:"min=18,max=120"`
}
to yield a complete zod schema with only the invocation:
schema := zod.InferSchema[User]()
JSON Schema Export
Each zod schema can later be serialized into standard JSON Schema format, making it straightforward to feed it into OpenAPI documentation pipelines or share with frontend developers bound by TypeScript types.
jsonSchema := schema.ToJSONSchema()
Transformation and Coercion
The library will gain built-in data manipulation operators, so a declaration such as:
validators.Object(map[string]zod.Schema{
"name": validators.String().Transform(strings.ToTitle),
"age": validators.String().Coerce().Number().Min(18),
})
will automatically title-case names and cast age inputs before numeric validation.
Plugin Ecosystem
Community authors will, over time, release add-on validators for niche domains; an example usage could be:
import "github.com/zod-go-plugins/financial"
schema := validators.Object(map[string]zod.Schema{
"creditCard": financial.CreditCard(),
"ssn": financial.SSN(),
})
IDE Integration and Code Generation
Plans are underway for a VS Code extension that highlights schema errors inline, completes struct tags, and even spins up a stub HTTP middleware layer based on the active schema design.
Command-line utilities now let developers extract schemas directly from running APIs. Such automation spares programmers the tedium of hand-coding type mirrors every time an endpoint shifts.
The new module plugs seamlessly into popular Go frameworks like Gin, Echo, and Fiber. One command wires existing handlers to a schema printer, turning live routes into machine-readable contracts on the fly.
Advanced Performance Optimizations
Researchers chasing speed have several fresh levers embedded in the release. Zero-allocation validation paths generated at build-time eliminate even transient memory waste.
String patterns now track processor SIMD lanes, cutting runtime char-by-char scans to single CPU cycles. Larger slices of log data validate against on-disk maps, removing the need to load everything into RAM.
Conclusion: Bridging Communities Through Innovation
This project began as a simple ports exercise but quickly turned into something bigger. It proved TypeScript's Zod ideas adapt smoothly, without sacrifice, to Go's lower-level idioms.
Long-term, the frontier of software won't be confined by syntax or runtime; it'll be defined by reusable concepts. zod-go is merely the first line of proof that good ideas jump fences without hesitation.
Get Started Today
Installation Command
In the terminal, run:
go get github.com/aymaneallaoui/zod-go
Minimal Program
package main
import (
"fmt"
"github.com/aymaneallaoui/zod-go/zod/validators"
)
func main() {
schema := validators.String().
Min(3).
Max(50).
Required()
if err := schema.Validate("Hello, zod-go!"); err != nil {
fmt.Printf("Validation failed: %v\n", err)
} else {
fmt.Println("Validation passed! 🎉")
}
}
Links
- GitHub Repository: https://github.com/aymaneallaoui/zod-go
-
Documentation: https://pkg.go.dev/github.com/aymaneallaoui/zod-go
Star the repository, try it in your projects, and join the community building the future of Go validation! ⭐
Top comments (2)
This is awesome, especially seeing Zod's fluent syntax come to Go without performance hits. Do you have a public branch or ETA for struct tag-based schema inference?
hello dotallio thanks
yeah here is the github repo
github.com/aymaneallaoui/zod-go
for the release i already have one release
for the struct tag-based schema inference still not supported rn but i'm working on it
Some comments may only be visible to logged-in visitors. Sign in to view all comments.