schema

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2026 License: MIT Imports: 15 Imported by: 0

README

Schema

tag Go Reference Go Report Card CI codecov License

Decode HTTP requests into Go structs with OpenAPI 3.0/3.1 compliance. Define your request structure with struct tags, and Schema handles the rest.

Features

  • OpenAPI 3.0/3.1 Compliant - Full support for all parameter serialization styles and locations
  • Unified API - Query, path, header, cookie parameters and request bodies in one call
  • Multiple Body Formats - JSON, XML, forms, multipart, file uploads
  • Performance Optimized - Metadata caching, zero allocations where possible
  • Extensible - Custom decoders, unmarshalers, and tag parsers
  • Type Safe - Automatic type conversion with validation

Installation

go get github.com/talav/schema

Quick Start

Define your request structure and decode in one line:

package main

import (
    "fmt"
    "net/http"
    "github.com/talav/schema"
)

type CreateUserRequest struct {
    // Path parameter (from router)
    OrgID string `schema:"org_id,location=path"`
    
    // Query parameter
    Version string `schema:"version" default:"v1"`
    
    // Header parameter
    APIKey string `schema:"X-API-Key,location=header"`
    
    // Request body (JSON, XML, or form - auto-detected)
    Body struct {
        Name  string `schema:"name"`
        Email string `schema:"email"`
        Age   int    `schema:"age"`
    } `body:"structured"`
}

func main() {
    // Create codec once, reuse across all requests
    codec := schema.NewDefaultCodec()
    
    http.HandleFunc("/orgs/{org_id}/users", func(w http.ResponseWriter, r *http.Request) {
        // Router params come from your router (chi, gorilla, etc.)
        routerParams := map[string]string{"org_id": "123"}
        
        var req CreateUserRequest
        if err := codec.DecodeRequest(r, routerParams, &req); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        
        // Request is decoded and validated
        fmt.Fprintf(w, "Creating user %s in org %s\n", req.Body.Name, req.OrgID)
    })
    
    http.ListenAndServe(":8080", nil)
}

Try it:

curl -X POST http://localhost:8080/orgs/acme/users?version=v2 \
  -H "X-API-Key: secret" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]", "age": 30}'

Documentation

Full Documentation

Getting Started
User Guides
Advanced
API Reference

Key Concepts

Parameter Locations

Parameters can be extracted from any part of the HTTP request:

type Request struct {
    ID      string `schema:"id,location=path"`        // /users/{id}
    Search  string `schema:"q,location=query"`        // ?q=golang
    APIKey  string `schema:"X-API-Key,location=header"` // Header: X-API-Key
    Session string `schema:"session,location=cookie"`   // Cookie: session=xyz
}

Learn more about parameters →

Request Bodies

Support for all common body formats with automatic content-type detection:

// JSON, XML, or URL-encoded forms
type JSONRequest struct {
    Body User `body:"structured"`
}

// File upload (small files)
type FileUpload struct {
    File []byte `body:"file"`
}

// File streaming (large files)
type StreamUpload struct {
    File io.ReadCloser `body:"file"`
}

// Multipart forms with files
type FormUpload struct {
    Body struct {
        Title    string        `schema:"title"`
        Document io.ReadCloser `schema:"document"`
    } `body:"multipart"`
}

// Multipart with file metadata (filename, size, content-type)
type MetadataUpload struct {
    Body struct {
        Title    string                `schema:"title"`
        Document *multipart.FileHeader `schema:"document"`
    } `body:"multipart"`
}

Learn more about request bodies →

OpenAPI Serialization Styles

Full support for OpenAPI 3.0/3.1 parameter serialization:

type Request struct {
    // Form style (default): ?ids=1&ids=2 or ?ids=1,2,3
    IDs []int `schema:"ids,style=form"`
    
    // Deep object: ?filter[status]=active&filter[type]=user
    Filter struct {
        Status string `schema:"status"`
        Type   string `schema:"type"`
    } `schema:"filter,style=deepObject"`
    
    // Space delimited: ?tags=go%20api%20http
    Tags []string `schema:"tags,style=spaceDelimited"`
}

Learn more about serialization →

Architecture

Schema consists of four main components:

  1. Codec - High-level API orchestrating the pipeline
  2. Metadata - Parses and caches struct tags (fast lookups)
  3. Decoder - Extracts HTTP parameters into maps
  4. Unmarshaler - Converts maps to typed structs (uses mapstructure)

Examples

With Chi Router
import (
    "github.com/go-chi/chi/v5"
    "github.com/talav/schema"
)

func main() {
    codec := schema.NewDefaultCodec()
    r := chi.NewRouter()
    
    r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        type Request struct {
            ID   string `schema:"id,location=path"`
            Page int    `schema:"page" default:"1"`
        }
        
        var req Request
        if err := codec.DecodeRequest(r, chi.RouteContext(r.Context()).URLParams, &req); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        
        // Use req.ID and req.Page
    })
    
    http.ListenAndServe(":8080", r)
}
File Upload with Streaming
type UploadRequest struct {
    Filename string        `schema:"filename"`
    File     io.ReadCloser `body:"file"` // Streams large files
}

func handler(w http.ResponseWriter, r *http.Request) {
    var req UploadRequest
    if err := codec.DecodeRequest(r, nil, &req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer req.File.Close()
    
    // Stream to disk
    out, _ := os.Create("/uploads/" + req.Filename)
    io.Copy(out, req.File)
    out.Close()
}
Multipart Form with Files
type FormRequest struct {
    Body struct {
        Title       string        `schema:"title"`
        Description string        `schema:"description"`
        Document    io.ReadCloser `schema:"document"`
    } `body:"multipart"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    var req FormRequest
    if err := codec.DecodeRequest(r, nil, &req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer req.Body.Document.Close()
    
    // Process form + file
}
Multipart with File Metadata

Use *multipart.FileHeader when you need access to filename, size, or content-type before reading:

type UploadRequest struct {
    Body struct {
        Title string                `schema:"title"`
        File  *multipart.FileHeader `schema:"file"`
    } `body:"multipart"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    var req UploadRequest
    if err := codec.DecodeRequest(r, nil, &req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Check metadata before reading
    if req.Body.File.Size > 10<<20 {
        http.Error(w, "file too large", http.StatusRequestEntityTooLarge)
        return
    }
    
    f, _ := req.Body.File.Open()
    defer f.Close()
    io.Copy(os.Stdout, f)
}

Extensibility

Customize behavior with your own decoders and unmarshalers:

// Custom decoder (e.g., add logging)
type LoggingDecoder struct {
    decoder schema.Decoder
}

func (d *LoggingDecoder) Decode(...) (map[string]any, error) {
    log.Println("Decoding request")
    return d.decoder.Decode(...)
}

// Custom unmarshaler (e.g., add validation)
type ValidatingUnmarshaler struct {
    unmarshaler schema.Unmarshaler
}

func (u *ValidatingUnmarshaler) Unmarshal(data map[string]any, result any) error {
    if err := u.unmarshaler.Unmarshal(data, result); err != nil {
        return err
    }
    // Add validation logic
    return validate(result)
}

// Use custom components
metadata := schema.NewDefaultMetadata()
decoder := NewLoggingDecoder(schema.NewDefaultDecoder())
unmarshaler := NewValidatingUnmarshaler(mapstructure.NewDefaultUnmarshaler())
codec := schema.NewCodec(metadata, unmarshaler, decoder)

Learn more about extensibility →

Testing

# Run all tests
go test ./...

# With race detector
go test -race ./...

# With coverage
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Contributing

Contributions are welcome!

Development Setup

Option 1: Local Go

Requirements: Go 1.18+

git clone https://github.com/talav/schema
cd schema
go mod download
go test ./...

Option 2: Dev Container

If you use VS Code, open the repository and it will prompt to reopen in a dev container with all tools pre-installed.

Making Changes
  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests: go test -race ./...
  5. Run linter: golangci-lint run (or it runs in CI)
  6. Commit with clear messages
  7. Push and open a Pull Request

License

MIT License - see LICENSE file for details.

Acknowledgments

Built on top of:

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultSchemaMetadata

func DefaultSchemaMetadata(field reflect.StructField, index int) any

NewDefaultSchemaMetadata creates default SchemaMetadata for untagged fields. This function is used by the metadata builder when no explicit schema tag is found.

func GetTagMetadata

func GetTagMetadata[T any](f *FieldMetadata, tagName string) (T, bool)

GetTagMetadata is a package-level generic function for type-safe access to tag metadata. Usage: GetTagMetadata[*SchemaMetadata](field, "schema").

func ParseBodyTag

func ParseBodyTag(field reflect.StructField, index int, tagValue string) (any, error)

ParseBodyTag parses a body tag and returns BodyMetadata.

func ParseSchemaTag

func ParseSchemaTag(field reflect.StructField, index int, tagValue string) (any, error)

ParseSchemaTag parses a schema tag and returns SchemaMetadata.

Types

type BodyMetadata

type BodyMetadata struct {
	MapKey   string
	BodyType BodyType
}

BodyMetadata represents metadata for body tag fields.

type BodyType

type BodyType string

BodyType represents the type of request body.

const (
	BodyTypeStructured BodyType = "structured" // JSON, XML
	BodyTypeFile       BodyType = "file"       // File upload
	BodyTypeMultipart  BodyType = "multipart"  // Multipart form
)

type Codec

type Codec struct {
	// contains filtered or unexported fields
}

Codec handles encoding and decoding between structs and parameter strings. It uses injectable decoder and unmarshaler for request handling.

func NewCodec

func NewCodec(metadata *Metadata, unmarshaler Unmarshaler, decoder Decoder) *Codec

NewCodec creates a new Codec with custom dependencies for advanced use cases.

func NewDefaultCodec

func NewDefaultCodec() *Codec

NewDefaultCodec creates a new Codec with default configuration. This is the recommended constructor for most use cases.

func (*Codec) DecodeRequest

func (c *Codec) DecodeRequest(request *http.Request, routerParams map[string]string, result any) error

DecodeRequest decodes an HTTP request into the provided struct. result must be a pointer to the target struct.

type Decoder

type Decoder interface {
	// Decode decodes HTTP parameters (query, header, cookie, path, body) to map.
	Decode(request *http.Request, routerParams map[string]string, metadata *StructMetadata) (map[string]any, error)
}

Decoder interface for decoding HTTP request data to maps.

func NewDecoder

func NewDecoder(metadata *Metadata, schemaTag string, bodyTag string) Decoder

newDefaultDecoder creates a new decoder.

func NewDefaultDecoder

func NewDefaultDecoder() Decoder

type DefaultMetadataFunc

type DefaultMetadataFunc func(field reflect.StructField, index int) any

DefaultMetadataFunc creates default metadata for untagged fields.

type FieldMetadata

type FieldMetadata struct {
	// StructFieldName is the name of the struct field in Go source code.
	StructFieldName string
	// Index is the field index in the struct (used for reflection-based field access).
	Index int
	// Embedded indicates whether this field is an embedded/anonymous struct field.
	Embedded bool
	// Type is the reflect.Type of the field.
	Type reflect.Type

	// Tag-specific metadata: tag name -> metadata object
	// A field can have multiple tags (e.g., schema + validate)
	TagMetadata map[string]any // "schema" -> *SchemaMetadata, "body" -> *BodyMetadata, etc.
}

FieldMetadata represents a cached struct field metadata. It can represent both parameter fields (schema tag) and body fields (body tag).

func (*FieldMetadata) HasTag

func (f *FieldMetadata) HasTag(tagName string) bool

HasTag checks if field has a specific tag.

type Metadata

type Metadata struct {
	// contains filtered or unexported fields
}

Metadata is a separate component for metadata operations (tag parsing, metadata building, caching). It serves use cases beyond Codec: - Validation metadata (for validators) - OpenAPI schema generation - Introspection and tooling.

func NewDefaultMetadata

func NewDefaultMetadata() *Metadata

NewDefaultMetadata creates a new Metadata with default parsers (schema and body).

func NewMetadata

func NewMetadata(registry *TagParserRegistry) *Metadata

NewMetadata creates a new Metadata with the given registry.

func (*Metadata) GetStructMetadata

func (m *Metadata) GetStructMetadata(typ reflect.Type) (*StructMetadata, error)

GetStructMetadata retrieves or builds struct metadata for the given type.

type ParameterLocation

type ParameterLocation string

ParameterLocation represents the location of a parameter in an OpenAPI spec.

const (
	// LocationQuery represents query parameters.
	LocationQuery ParameterLocation = "query"
	// LocationPath represents path parameters.
	LocationPath ParameterLocation = "path"
	// LocationHeader represents header parameters.
	LocationHeader ParameterLocation = "header"
	// LocationCookie represents cookie parameters.
	LocationCookie ParameterLocation = "cookie"
)

type SchemaMetadata

type SchemaMetadata struct {
	ParamName string
	MapKey    string
	Location  ParameterLocation
	Style     Style
	Explode   bool
}

SchemaMetadata represents metadata for schema tag fields.

type StructMetadata

type StructMetadata struct {
	Type   reflect.Type
	Fields []FieldMetadata
	// contains filtered or unexported fields
}

func NewStructMetadata

func NewStructMetadata(typ reflect.Type, fields []FieldMetadata) (*StructMetadata, error)

NewStructMetadata creates a new struct metadata from a type and fields. This is useful for tests and when you already have FieldMetadata built.

func (*StructMetadata) Field

func (m *StructMetadata) Field(fieldName string) (*FieldMetadata, bool)

Field returns FieldMetadata by field name.

type Style

type Style string

Style represents the serialization style for a parameter.

const (
	// StyleMatrix is used for path parameters.
	// Values are prefixed with a semicolon (;) and key-value pairs are separated by an equals sign (=).
	StyleMatrix Style = "matrix"
	// StyleLabel is used for path parameters.
	// Values are prefixed with a period (.).
	StyleLabel Style = "label"
	// StyleForm is commonly used for query and cookie parameters.
	// Values are serialized as form data.
	StyleForm Style = "form"
	// StyleSimple is applicable to path and header parameters.
	// Values are serialized without any additional formatting.
	StyleSimple Style = "simple"
	// StyleSpaceDelimited is used for query parameters.
	// Array values are separated by spaces.
	StyleSpaceDelimited Style = "spaceDelimited"
	// StylePipeDelimited is used for query parameters.
	// Array values are separated by pipes (|).
	StylePipeDelimited Style = "pipeDelimited"
	// StyleDeepObject is used for query parameters.
	// Allows for complex objects to be represented in a deep object style.
	StyleDeepObject Style = "deepObject"
)

type TagParserFunc

type TagParserFunc func(field reflect.StructField, index int, tagValue string) (any, error)

TagParserFunc is a function type for parsing struct tags into metadata.

type TagParserRegistry

type TagParserRegistry struct {
	// contains filtered or unexported fields
}

TagParserRegistry manages registered tag parsers with explicit tag name mapping. It is immutable after construction.

func NewDefaultTagParserRegistry

func NewDefaultTagParserRegistry() *TagParserRegistry

NewDefaultTagParserRegistry creates a new tag parser registry with default parsers (schema and body).

func NewTagParserRegistry

func NewTagParserRegistry(opts ...TagParserRegistryOption) *TagParserRegistry

NewTagParserRegistry creates a new immutable tag parser registry with the given options.

func (*TagParserRegistry) All

func (r *TagParserRegistry) All() map[string]TagParserFunc

All returns a copy of all registered parsers.

func (*TagParserRegistry) Get

func (r *TagParserRegistry) Get(tagName string) TagParserFunc

Get returns the parser for the given tag name, or nil if not found.

func (*TagParserRegistry) GetDefault

func (r *TagParserRegistry) GetDefault(tagName string) DefaultMetadataFunc

GetDefault returns the default metadata factory for the given tag name, or nil if not found.

type TagParserRegistryOption

type TagParserRegistryOption func(parsers map[string]TagParserFunc, defaults map[string]DefaultMetadataFunc)

TagParserRegistryOption configures a TagParserRegistry during construction.

func WithTagParser

func WithTagParser(tagName string, parser TagParserFunc, defaultFunc ...DefaultMetadataFunc) TagParserRegistryOption

WithTagParser registers a parser with an explicit tag name. If parser is nil, it is skipped. If tag already exists, it is overridden. An optional default metadata function can be provided as a third parameter.

type Unmarshaler

type Unmarshaler interface {
	// Unmarshal transforms map[string]any into a Go struct pointed to by result.
	Unmarshal(data map[string]any, result any) error
}

Unmarshaler interface for unmarshaling maps to Go structs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL