Cast Package

The cast package provides type-safe conversion utilities with error handling for converting between Go types, especially useful when working with any (any) values from JSON, maps, or dynamic data sources.

Table of Contents

Overview

Import Path: github.com/primadi/lokstra/common/cast

Key Features:

✓ Type-Safe Conversion    - Convert with compile-time safety
✓ Error Handling          - Explicit error returns
✓ Struct Mapping          - Map to struct with caching
✓ Generic Support         - ToType[T] for any type
✓ Nested Structures       - Handle complex nested data
✓ Performance Optimized   - Field mapping cache

Installation

import "github.com/primadi/lokstra/common/cast"

Basic Conversions

ToInt

Convert any value to int:

// From various numeric types
age, err := cast.ToInt(42)           // int: 42
age, err = cast.ToInt(int64(100))    // int: 100
age, err = cast.ToInt(uint8(25))     // int: 25

// From nil (returns 0)
age, err = cast.ToInt(nil)           // int: 0, nil

// From incompatible type (returns error)
age, err = cast.ToInt("not a number") // int: 0, error

Supported Types:

ToFloat64

Convert any value to float64:

// From various numeric types
price, err := cast.ToFloat64(99.99)      // float64: 99.99
price, err = cast.ToFloat64(100)         // float64: 100.0
price, err = cast.ToFloat64(float32(50)) // float64: 50.0

// From nil (returns 0)
price, err = cast.ToFloat64(nil)         // float64: 0.0, nil

Supported Types:

ToTime

Convert any value to time.Time:

// From string (multiple formats supported)
created, err := cast.ToTime("2024-01-15 14:30:00")  // time.DateTime
created, err = cast.ToTime("2024-01-15")            // time.DateOnly
created, err = cast.ToTime("14:30:00")              // time.TimeOnly
created, err = cast.ToTime("2024-01-15T14:30:00Z")  // time.RFC3339

// From Unix timestamp
created, err = cast.ToTime(int64(1705328400))       // Unix seconds
created, err = cast.ToTime(float64(1705328400))     // Unix seconds

// From time.Time (no conversion)
created, err = cast.ToTime(time.Now())              // Pass through

// From nil (returns zero time)
created, err = cast.ToTime(nil)                     // time.Time{}, nil

Supported Formats:

Generic Conversion

ToType[T]

Convert any value to a specific type using generics:

// Basic types
age, err := cast.ToType[int](userData["age"], false)
price, err := cast.ToType[float64](productData["price"], false)
created, err := cast.ToType[time.Time](data["created_at"], false)

// Structs
user, err := cast.ToType[User](userData, false)
userPtr, err := cast.ToType[*User](userData, false)

// Slices
users, err := cast.ToType[[]User](usersData, false)

Parameters:

Strict Mode:

// Non-strict (default) - ignores unknown fields
user, err := cast.ToType[User](data, false)  // OK, extra fields ignored

// Strict - fails on unknown fields
user, err := cast.ToType[User](data, true)   // Error if data has extra fields

Struct Conversion

ToStruct

Convert map[string]any to struct:

type User struct {
    ID       int       `json:"id"`
    Username string    `json:"username"`
    Email    string    `json:"email"`
    Age      int       `json:"age"`
    Active   bool      `json:"active"`
    Created  time.Time `json:"created_at"`
}

data := map[string]any{
    "id":         123,
    "username":   "john_doe",
    "email":      "john@example.com",
    "age":        30,
    "active":     true,
    "created_at": "2024-01-15T10:30:00Z",
}

var user User
err := cast.ToStruct(data, &user, false)
if err != nil {
    log.Fatal(err)
}

// user.ID = 123
// user.Username = "john_doe"
// user.Email = "john@example.com"
// user.Age = 30
// user.Active = true
// user.Created = parsed time

Nested Structs

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type User struct {
    ID      int     `json:"id"`
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

data := map[string]any{
    "id":   123,
    "name": "John Doe",
    "address": map[string]any{
        "street":  "123 Main St",
        "city":    "New York",
        "country": "USA",
    },
}

var user User
err := cast.ToStruct(data, &user, false)

// user.Address.Street = "123 Main St"
// user.Address.City = "New York"
// user.Address.Country = "USA"

Pointer Fields

type User struct {
    ID      int      `json:"id"`
    Name    string   `json:"name"`
    Address *Address `json:"address,omitempty"`
}

// With address
data := map[string]any{
    "id":   123,
    "name": "John",
    "address": map[string]any{
        "city": "NYC",
    },
}
var user1 User
cast.ToStruct(data, &user1, false)
// user1.Address != nil

// Without address
data = map[string]any{
    "id":   123,
    "name": "John",
}
var user2 User
cast.ToStruct(data, &user2, false)
// user2.Address == nil

Slice Fields

type User struct {
    ID    int      `json:"id"`
    Name  string   `json:"name"`
    Tags  []string `json:"tags"`
    Roles []Role   `json:"roles"`
}

type Role struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

data := map[string]any{
    "id":   123,
    "name": "John",
    "tags": []any{"admin", "verified", "premium"},
    "roles": []any{
        map[string]any{"id": 1, "name": "admin"},
        map[string]any{"id": 2, "name": "user"},
    },
}

var user User
err := cast.ToStruct(data, &user, false)

// user.Tags = ["admin", "verified", "premium"]
// user.Roles = [{ID: 1, Name: "admin"}, {ID: 2, Name: "user"}]

Field Mapping

The package uses JSON tags for field mapping and caches field information for performance:

type User struct {
    ID       int    `json:"id"`           // Maps to "id"
    Username string `json:"username"`     // Maps to "username"
    FullName string `json:"full_name"`    // Maps to "full_name"
    Internal string                       // Maps to "Internal" (field name)
    Ignored  string `json:"-"`            // Ignored
}

Caching:

Slice Conversion

SliceConvert

Convert slice of one type to slice of another:

// Convert []any to []string
source := []any{"one", "two", "three"}
result, err := cast.SliceConvert[[]string](source)
// result = []string{"one", "two", "three"}

// Convert []any to []int
source := []any{1, 2, 3}
result, err := cast.SliceConvert[[]int](source)
// result = []int{1, 2, 3}

// Type mismatch (error)
source := []any{"one", 2, "three"}
result, err := cast.SliceConvert[[]string](source)
// err: cannot assign value of type int to string at index 1

Requirements:

Utility Functions

IsEmpty

Check if a value is considered empty:

// Strings
cast.IsEmpty("")           // true
cast.IsEmpty("hello")      // false

// Numbers
cast.IsEmpty(0)            // true
cast.IsEmpty(42)           // false
cast.IsEmpty(0.0)          // true

// Booleans
cast.IsEmpty(false)        // true
cast.IsEmpty(true)         // false

// Slices
cast.IsEmpty([]int{})      // true
cast.IsEmpty([]int{1, 2})  // false

// Maps
cast.IsEmpty(map[string]int{})           // true
cast.IsEmpty(map[string]int{"a": 1})     // false

// Pointers
var ptr *int
cast.IsEmpty(ptr)          // true
ptr = new(int)
cast.IsEmpty(ptr)          // false

// Nil
cast.IsEmpty(nil)          // true

// Structs (all fields empty)
type User struct {
    Name string
    Age  int
}
cast.IsEmpty(User{})       // true
cast.IsEmpty(User{Name: "John"})  // false

Best Practices

Error Handling

 DO: Always check errors from conversion functions
age, err := cast.ToInt(value)
if err != nil {
    return fmt.Errorf("invalid age: %w", err)
}

 DON'T: Ignore errors
age, _ := cast.ToInt(value)  // BAD: Silent failure

Struct Conversion

 DO: Use strict mode for API validation
err := cast.ToStruct(requestData, &request, true)
if err != nil {
    return BadRequest("Invalid request structure")
}

 DO: Use non-strict mode for flexible data sources
err := cast.ToStruct(configData, &config, false)

 DON'T: Use ToStruct without pointer
err := cast.ToStruct(data, user, false)  // BAD: Pass &user instead

Type Safety

 DO: Use ToType[T] for type-safe conversions
age, err := cast.ToType[int](value, false)

 DON'T: Use type assertions directly
age := value.(int)  // BAD: Panics on wrong type

Performance

 DO: Reuse struct types to benefit from caching
var user User
cast.ToStruct(data1, &user, false)
cast.ToStruct(data2, &user, false)  // Uses cached field mapping

 DO: Use appropriate conversion functions
age, err := cast.ToInt(value)  // More efficient than ToType[int]

 DON'T: Build structs in loops without consideration
for _, data := range hugeDataset {
    cast.ToStruct(data, &result, false)  // OK: Cache makes this efficient
}

Nil Handling

 DO: Handle nil values appropriately
value, err := cast.ToInt(nil)  // Returns 0, nil

 DO: Check for zero values after conversion
if value == 0 && originalValue == nil {
    // Handle nil case
}

 DON'T: Assume non-nil returns
value, _ := cast.ToInt(possiblyNil)  // Could be 0 from nil or actual 0

Examples

HTTP Request Parsing

func CreateUser(w http.ResponseWriter, r *http.Request) {
    var requestData map[string]any
    json.NewDecoder(r.Body).Decode(&requestData)
    
    type CreateUserRequest struct {
        Username string `json:"username" validate:"required"`
        Email    string `json:"email" validate:"required,email"`
        Age      int    `json:"age" validate:"gte=18"`
    }
    
    var req CreateUserRequest
    err := cast.ToStruct(requestData, &req, true)
    if err != nil {
        http.Error(w, "Invalid request structure", http.StatusBadRequest)
        return
    }
    
    // Validate
    fieldErrors, err := validator.ValidateStruct(&req)
    if len(fieldErrors) > 0 {
        // Return validation errors
        return
    }
    
    // Process request
    user := createUser(req)
    json.NewEncoder(w).Encode(user)
}

Configuration Parsing

func LoadConfig(configData map[string]any) (*Config, error) {
    type DatabaseConfig struct {
        Host     string `json:"host"`
        Port     int    `json:"port"`
        Database string `json:"database"`
        Username string `json:"username"`
        Password string `json:"password"`
    }
    
    type Config struct {
        AppName  string         `json:"app_name"`
        Port     int            `json:"port"`
        Database DatabaseConfig `json:"database"`
    }
    
    var config Config
    err := cast.ToStruct(configData, &config, false)
    if err != nil {
        return nil, fmt.Errorf("invalid config: %w", err)
    }
    
    return &config, nil
}

Database Result Mapping

func GetUsers(ctx context.Context) ([]User, error) {
    // Get rows from database as []map[string]any
    rows, err := conn.SelectManyRowMap(ctx, "SELECT * FROM users")
    if err != nil {
        return nil, err
    }
    
    users := make([]User, 0, len(rows))
    for _, row := range rows {
        var user User
        if err := cast.ToStruct(row, &user, false); err != nil {
            return nil, fmt.Errorf("failed to map user: %w", err)
        }
        users = append(users, user)
    }
    
    return users, nil
}

Dynamic Type Conversion

func ProcessField(fieldType string, value any) (any, error) {
    switch fieldType {
    case "int":
        return cast.ToType[int](value, false)
    case "float":
        return cast.ToType[float64](value, false)
    case "time":
        return cast.ToType[time.Time](value, false)
    case "string":
        return fmt.Sprintf("%v", value), nil
    default:
        return nil, fmt.Errorf("unsupported type: %s", fieldType)
    }
}

Service Factory Pattern

func ServiceFactory(params map[string]any) (Service, error) {
    type ServiceConfig struct {
        Host    string        `json:"host"`
        Port    int           `json:"port"`
        Timeout time.Duration `json:"timeout"`
        Options map[string]any `json:"options"`
    }
    
    var config ServiceConfig
    err := cast.ToStruct(params, &config, false)
    if err != nil {
        return nil, fmt.Errorf("invalid service config: %w", err)
    }
    
    return NewService(&config), nil
}

Form Data Processing

func ProcessForm(formData map[string][]string) (*FormData, error) {
    // Convert form values (first value of each field)
    data := make(map[string]any)
    for key, values := range formData {
        if len(values) > 0 {
            data[key] = values[0]
        }
    }
    
    type FormData struct {
        Name    string `json:"name"`
        Email   string `json:"email"`
        Age     int    `json:"age"`
        Message string `json:"message"`
    }
    
    var form FormData
    err := cast.ToStruct(data, &form, false)
    if err != nil {
        return nil, fmt.Errorf("invalid form data: %w", err)
    }
    
    return &form, nil
}

Next: Utils Package - General utility functions