Advanced JSON Handling
Shows how to precisely shape JSON payloads for a user model while keeping Go types safe.
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