main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"time"
)

// User represents a user with custom JSON handling
type User struct {
	ID        int       `json:"id"`
	Username  string    `json:"username,omitempty"` // Omit if empty
	Email     string    `json:"email"`
	Password  string    `json:"-" ` // Exclude from JSON entirely
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt *time.Time `json:"updated_at,omitempty"` // Null if nil
	Age       int       `json:"age,string"` // Encode as string
}

// Custom marshaler to format CreatedAt as RFC3339
func (u User) MarshalJSON() ([]byte, error) {
	// Create a temporary type to avoid infinite recursion
	type Alias User
	return json.Marshal(&struct {
		*Alias
		CreatedAt string `json:"created_at"`
	}{
		Alias:     (*Alias)(&u),
		CreatedAt: u.CreatedAt.Format(time.RFC3339),
	})
}

// Custom unmarshaler to parse CreatedAt from RFC3339
func (u *User) UnmarshalJSON(data []byte) error {
	// Create a temporary type to avoid infinite recursion
	type Alias User
	aux := &struct {
		*Alias
		CreatedAt string `json:"created_at"`
	}{
		Alias: (*Alias)(u),
	}

	if err := json.Unmarshal(data, &aux); err != nil {
		return fmt.Errorf("failed to unmarshal user: %w", err)
	}

	// Parse CreatedAt from string to time.Time
	parsedTime, err := time.Parse(time.RFC3339, aux.CreatedAt)
	if err != nil {
		return fmt.Errorf("invalid created_at format (expected RFC3339): %w", err)
	}
	u.CreatedAt = parsedTime

	// Validate required fields
	if u.Email == "" {
		return errors.New("email is required")
	}

	return nil
}

// Validate checks user data for validity
func (u User) Validate() error {
	if u.Username == "" {
		return errors.New("username cannot be empty")
	}
	if u.Age < 0 || u.Age > 150 {
		return errors.New("age must be between 0 and 150")
	}
	return nil
}

func main() {
	// Create a sample user
	updatedAt := time.Now()
	user := User{
		ID:        1,
		Username:  "johndoe",
		Email:     "[email protected]",
		Password:  "secret123", // Will be excluded from JSON
		CreatedAt: time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC),
		UpdatedAt: &updatedAt,
		Age:       30,
	}

	// Validate user before serialization
	if err := user.Validate(); err != nil {
		log.Fatalf("User validation failed: %v", err)
	}

	// Marshal user to JSON
	jsonData, err := json.MarshalIndent(user, "", "  ")
	if err != nil {
		log.Fatalf("Failed to marshal user to JSON: %v", err)
	}
	fmt.Println("Serialized User JSON:")
	fmt.Println(string(jsonData))

	// Example JSON input (simulate API request)
	jsonInput := `{
		"id": 1,
		"email": "[email protected]",
		"created_at": "2023-01-01T12:00:00Z",
		"age": "30"
	}`

	// Unmarshal JSON back to User
	var parsedUser User
	if err := json.Unmarshal([]byte(jsonInput), &parsedUser); err != nil {
		log.Fatalf("Failed to unmarshal JSON to user: %v", err)
	}

	fmt.Println("\nParsed User:")
	fmt.Printf("ID: %d\n", parsedUser.ID)
	fmt.Printf("Email: %s\n", parsedUser.Email)
	fmt.Printf("CreatedAt: %v\n", parsedUser.CreatedAt)
	fmt.Printf("Age: %d\n", parsedUser.Age)

	// Test omitempty behavior (empty username)
	emptyUser := User{
		ID:    2,
		Email: "[email protected]",
		Age:   25,
	}
	emptyUserJSON, _ := json.MarshalIndent(emptyUser, "", "  ")
	fmt.Println("\nUser with empty username (omitempty):")
	fmt.Println(string(emptyUserJSON))
}

How It Works

Shows how to precisely shape JSON payloads for a user model while keeping Go types safe.

Struct tags exclude the password, omit empty usernames, and encode age as a string; custom MarshalJSON formats timestamps and preserves optional UpdatedAt pointers; custom UnmarshalJSON validates required fields and converts the string-encoded age back to an int before constructing the struct; sample code marshals, unmarshals, and demonstrates omitempty behavior.

Key Concepts

  • 1Combines omitempty, json:"-" and string tags to tune output.
  • 2Custom marshal and unmarshal pair enforces formatting and validation rules.
  • 3Optional fields are pointers so null versus missing can be distinguished.
  • 4Examples illustrate round-tripping and the impact of tags on the payload.

When to Use This Pattern

  • APIs that must hide secrets while keeping legacy field formats.
  • Interfacing with clients that send numbers as strings.
  • Schema migrations where nullable versus missing values matter.
  • Validating inbound JSON before persisting to storage.

Best Practices

  • Keep marshal and unmarshal logic symmetrical to avoid drift.
  • Prefer pointers for optional or nullable fields.
  • Validate required fields early and wrap errors with field context.
  • Avoid leaking secrets by marking them json:"-" and keeping them out of logs.
Go Version1.16+
Difficultyintermediate
Production ReadyYes
Lines of Code131