Sensitive Fields

Automatically clean sensitive data from API responses

Forge can automatically clean sensitive data from API responses before they are serialized to JSON. This prevents accidental exposure of passwords, API keys, tokens, and other confidential fields in your API responses.

The sensitive Struct Tag

Mark fields with the sensitive struct tag to control how they are cleaned. Three modes are available:

Tag ValueBehaviorOutput
sensitive:"true"Set to zero value"" for strings, 0 for numbers, nil for pointers
sensitive:"redact"Replace with [REDACTED]"[REDACTED]"
sensitive:"mask:***"Replace with a custom mask"***" (or whatever mask you specify)

Enabling Sensitive Field Cleaning

Sensitive field cleaning is opt-in per route. Enable it with WithSensitiveFieldCleaning().

app.GET("/users/:id", getUser, forge.WithSensitiveFieldCleaning())

Sensitive field cleaning only works with the opinionated and combined handler patterns that return response structs. If you manually call ctx.JSON(), the cleaning is not applied. Use a response struct to benefit from automatic cleaning.

Basic Example

Define a response struct with sensitive tags:

type UserResponse struct {
    ID        string `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Password  string `json:"password" sensitive:"true"`
    APIKey    string `json:"api_key" sensitive:"redact"`
    SSN       string `json:"ssn" sensitive:"mask:***-**-****"`
}

Register a handler that returns this struct:

app.GET("/users/:id", func(ctx forge.Context, req *GetUserRequest) (*UserResponse, error) {
    user, err := store.GetByID(ctx.Context(), req.ID)
    if err != nil {
        return nil, forge.NotFound("user not found")
    }

    return &UserResponse{
        ID:       user.ID,
        Name:     user.Name,
        Email:    user.Email,
        Password: user.Password,  // Will be cleaned
        APIKey:   user.APIKey,    // Will be cleaned
        SSN:      user.SSN,       // Will be cleaned
    }, nil
}, forge.WithSensitiveFieldCleaning())

The JSON response sent to the client:

{
  "id": "usr_abc123",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "password": "",
  "api_key": "[REDACTED]",
  "ssn": "***-**-****"
}

Cleaning Modes

Zero Value (sensitive:"true")

Replaces the field with its zero value. For strings this is "", for numbers 0, for booleans false, and for pointers/slices nil.

type TokenResponse struct {
    AccessToken  string `json:"access_token" sensitive:"true"`
    RefreshToken string `json:"refresh_token" sensitive:"true"`
    ExpiresIn    int    `json:"expires_in"`
}

Response:

{
  "access_token": "",
  "refresh_token": "",
  "expires_in": 3600
}

This mode is useful when you want the field present in the response schema but empty in practice, or when the frontend uses the presence of the field structurally.

Redaction (sensitive:"redact")

Replaces the field value with the string [REDACTED]. This makes it clear that a value exists but has been intentionally hidden.

type AccountResponse struct {
    ID          string `json:"id"`
    Email       string `json:"email"`
    PhoneNumber string `json:"phone_number" sensitive:"redact"`
    TaxID       string `json:"tax_id" sensitive:"redact"`
}

Response:

{
  "id": "acc_123",
  "email": "user@example.com",
  "phone_number": "[REDACTED]",
  "tax_id": "[REDACTED]"
}

Custom Mask (sensitive:"mask:...")

Replaces the field value with a custom mask string. The text after mask: is used verbatim.

type PaymentResponse struct {
    ID         string `json:"id"`
    CardNumber string `json:"card_number" sensitive:"mask:****-****-****-****"`
    CVV        string `json:"cvv" sensitive:"mask:***"`
    Amount     int    `json:"amount"`
}

Response:

{
  "id": "pay_456",
  "card_number": "****-****-****-****",
  "cvv": "***",
  "amount": 5000
}

This is useful for showing a placeholder that indicates the shape of the original data.

How It Works

When WithSensitiveFieldCleaning() is enabled on a route, the router processes the response struct before JSON serialization:

The handler returns a response struct (using the opinionated or combined handler pattern).

The router inspects the struct's fields via reflection, looking for the sensitive tag.

Tagged fields are modified according to their mode: zero value, redaction, or custom mask.

The cleaned struct is serialized to JSON and sent to the client.

The cleaning happens in-memory on a copy of the response data. Your original data structures are not modified.

Nested Structs

Sensitive field cleaning works recursively on nested structs.

type CompanyResponse struct {
    ID      string          `json:"id"`
    Name    string          `json:"name"`
    Owner   OwnerInfo       `json:"owner"`
    Banking BankingInfo     `json:"banking"`
}

type OwnerInfo struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    SSN   string `json:"ssn" sensitive:"mask:***-**-****"`
}

type BankingInfo struct {
    BankName      string `json:"bank_name"`
    AccountNumber string `json:"account_number" sensitive:"redact"`
    RoutingNumber string `json:"routing_number" sensitive:"redact"`
}

Response:

{
  "id": "comp_789",
  "name": "Acme Corp",
  "owner": {
    "name": "John Smith",
    "email": "john@acme.com",
    "ssn": "***-**-****"
  },
  "banking": {
    "bank_name": "First National",
    "account_number": "[REDACTED]",
    "routing_number": "[REDACTED]"
  }
}

Combining with Other Options

Sensitive field cleaning works alongside all other route options.

app.GET("/users/:id", getUser,
    forge.WithName("get-user"),
    forge.WithTags("users"),
    forge.WithAuth("jwt"),
    forge.WithSensitiveFieldCleaning(),
    forge.WithResponseSchema(200, "User details", &UserResponse{}),
)

When to Use Each Mode

ModeUse Case
sensitive:"true"Internal tokens, passwords, secrets that should never be exposed
sensitive:"redact"Fields where you want to indicate a value exists but is hidden
sensitive:"mask:..."Fields where a format hint is helpful (card numbers, phone numbers, SSNs)

Best Practices

Always use sensitive tags on response structs, not on database models. Keep your internal models clean and apply cleaning at the API boundary.

// Internal model -- no sensitive tags
type User struct {
    ID       string
    Name     string
    Email    string
    Password string
    APIKey   string
}

// API response -- sensitive tags applied here
type UserResponse struct {
    ID       string `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password" sensitive:"true"`
    APIKey   string `json:"api_key" sensitive:"redact"`
}

Enable cleaning on all routes that return sensitive data. Use group-level patterns or controllers to apply it consistently.

// In a controller, apply to all user-related endpoints
func (c *UserController) Routes(r forge.Router) error {
    r.GET("/:id", c.get,
        forge.WithSensitiveFieldCleaning(),
    )
    r.GET("/", c.list,
        forge.WithSensitiveFieldCleaning(),
    )
    return nil
}

Prefer redaction over zero values for audit trails. When debugging or logging, [REDACTED] makes it clear the field was intentionally cleaned, whereas an empty string could be confused with a missing value.

Use masks for fields that need format hints. If the frontend displays a masked credit card number like ****-****-****-1234, use the mask mode to provide that structure.

How is this guide?

On this page