DEV Community

Cover image for Embracing TypeScript Principles in Go: The Creation of a Zod-Inspired Validation Library
aymane aallaoui
aymane aallaoui

Posted on

Embracing TypeScript Principles in Go: The Creation of a Zod-Inspired Validation Library

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
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

Concurrent Processing:

Heavy workloads benefit from parallelism:

results := zod.ValidateConcurrently(schemas, dataList, 4) // launches four goroutines and aggregates outcomes
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}$`),
})
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

to yield a complete zod schema with only the invocation:

schema := zod.InferSchema[User]()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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),
})
Enter fullscreen mode Exit fullscreen mode

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(),
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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! 🎉")
    }
}
Enter fullscreen mode Exit fullscreen mode

Links

Star the repository, try it in your projects, and join the community building the future of Go validation! ⭐

Top comments (2)

Collapse
 
dotallio profile image
Dotallio

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?

Collapse
 
aymanepraxe profile image
aymane aallaoui

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.