Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Validation

Validation

go.putnami.dev/schema provides struct-tag-based validation with type coercion and default values.

Validating data

Validate a map[string]any against a Go struct type:

import (
    "reflect"
    "go.putnami.dev/schema"
)

type CreateUserInput struct {
    Name  string `json:"name" validate:"required,minlen=2,maxlen=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=0,max=150"`
}

result := schema.Validate(
    reflect.TypeOf(CreateUserInput{}),
    map[string]any{
        "name":  "Jane",
        "email": "not-an-email",
        "age":   -5,
    },
)

if result.HasErrors() {
    for _, err := range result.Errors {
        fmt.Printf("%s: %s\n", err.Field, err.Message)
        // email: must be a valid email address
        // age: must be at least 0
    }
}

Validation tags

Add validation rules to struct fields using the validate tag:

type Product struct {
    ID          string  `json:"id" validate:"required,uuid"`
    Name        string  `json:"name" validate:"required,minlen=1,maxlen=200"`
    Description string  `json:"description" validate:"maxlen=2000"`
    Price       float64 `json:"price" validate:"required,min=0"`
    Category    string  `json:"category" validate:"required,oneof=electronics|clothing|food"`
    URL         string  `json:"url" validate:"url"`
}

Available validators

Tag Description Example
required Must be present and non-zero validate:"required"
uuid UUID v4 format validate:"uuid"
email Valid email address validate:"email"
url Valid URL validate:"url"
min=N Minimum numeric value validate:"min=0"
max=N Maximum numeric value validate:"max=100"
minlen=N Minimum string length validate:"minlen=2"
maxlen=N Maximum string length validate:"maxlen=255"
pattern=REGEX Must match regular expression validate:"pattern=^[a-z]+$"
oneof=a|b|c Must be one of the listed values validate:"oneof=active|inactive"

Combine multiple validators with commas:

validate:"required,minlen=2,maxlen=100"

Default values

Set default values for optional fields:

type SearchParams struct {
    Query    string `json:"query" validate:"required"`
    Page     int    `json:"page" default:"1" validate:"min=1"`
    PageSize int    `json:"pageSize" default:"20" validate:"min=1,max=100"`
    Sort     string `json:"sort" default:"relevance" validate:"oneof=relevance|date|price"`
}

Defaults are applied before validation, so fields with defaults are always populated.

Type coercion

Enable automatic type coercion to convert string values to their target types:

result := schema.Validate(
    reflect.TypeOf(SearchParams{}),
    map[string]any{
        "query":    "shoes",
        "page":     "2",      // string → int
        "pageSize": "50",     // string → int
    },
    schema.WithCoerce(),
)

// result.Data["page"] == 2 (int, not "2")

Validation result

type Result struct {
    Data   map[string]any   // Validated and coerced values
    Errors []FieldError     // Validation errors
}

type FieldError struct {
    Field   string // Field name
    Message string // Error description
}

result.HasErrors() // true if any validation errors

Validation options

Option Description
WithCoerce() Enable automatic type coercion (string → int, etc.)
WithLabel(prefix) Prefix error field names (e.g., "body." → "body.name")

Using with HTTP endpoints

Manual validation

func createUser(ctx *fhttp.Context) *fhttp.Response {
    var input map[string]any
    if err := ctx.Body(&input); err != nil {
        return fhttp.JSONStatus(400, map[string]string{"error": "invalid JSON"})
    }

    result := schema.Validate(
        reflect.TypeOf(CreateUserInput{}),
        input,
        schema.WithCoerce(),
    )
    if result.HasErrors() {
        return fhttp.JSONStatus(422, map[string]any{"errors": result.Errors})
    }

    // Use result.Data for validated values
    name := result.Data["name"].(string)
    email := result.Data["email"].(string)
    // ...
}

With the endpoint builder

The endpoint builder integrates validation automatically:

endpoint := fhttp.Endpoint("POST", "/users").
    Body(reflect.TypeOf(CreateUserInput{})).
    Handle(func(ctx *fhttp.EndpointContext) *fhttp.Response {
        // ctx.ValidatedBody contains validated and coerced data
        name := ctx.ValidatedBody["name"].(string)
        return fhttp.JSONStatus(201, map[string]string{"name": name})
    })

See HTTP & Middleware for details.

Nested struct validation

Nested structs are validated recursively:

type Address struct {
    Street string `json:"street" validate:"required"`
    City   string `json:"city" validate:"required"`
    Zip    string `json:"zip" validate:"required,pattern=^[0-9]{5}$"`
}

type CreateUserInput struct {
    Name    string  `json:"name" validate:"required"`
    Email   string  `json:"email" validate:"required,email"`
    Address Address `json:"address" validate:"required"`
}

Related guides

  • HTTP & Middleware — endpoint validation
  • OpenAPI — validation tags in API specs
  • Errors — validation error responses

On this page

  • Validation
  • Validating data
  • Validation tags
  • Available validators
  • Default values
  • Type coercion
  • Validation result
  • Validation options
  • Using with HTTP endpoints
  • Manual validation
  • With the endpoint builder
  • Nested struct validation
  • Related guides