Navigating Type Safety in Go When Working with JWTs and JSON

Navigating Type Safety in Go When Working with JWTs and JSON
Photo by 三山 / Unsplash

When moving from PHP to Go, one of the first things developers notice is how different the two languages treat types. PHP is famously forgiving: it will happily coerce strings into numbers, numbers into booleans, and so on, often without you realizing it. Go, on the other hand, is strict and explicit. It will not let you mix types without you acknowledging the conversion.

This difference becomes very important when working with JWTs and JSON data, because most JWT payloads are just JSON objects under the hood. Handling them correctly requires an awareness of Go’s type system, especially when dealing with interfaces, numbers, and strings.


PHP vs Go: Type Handling

In Go:

var value interface{} = "123"
result := value + 1 // ❌ Compile error

Go refuses to compile this because interface{} is not known to be a number. You must assert or convert explicitly.

In PHP:

$value = "123";
$result = $value + 1; // Works fine, result is 124

PHP quietly converts the string "123" into an integer.


JSON in Go: Why Everything Becomes interface{}

When unmarshalling JSON into a map[string]interface{}, Go has no way to know the exact types of the fields. The encoding/json package uses a set of defaults:

  • Stringsstring
  • Numbersfloat64
  • Booleansbool
  • Objectsmap[string]interface{}
  • Arrays[]interface{}

This means that a JWT claim like "exp": 1712345678 (an integer) becomes a float64 in Go. If you expect an int64, you must cast carefully.


Working with JWT Claims in Go

Type Assertions for Strings

sub, ok := claims["sub"].(string)
if !ok {
    return errors.New("invalid sub claim: must be a string")
}

Here, if the type is not string, the assertion fails and you handle it gracefully.

Type Assertions for Numbers

expFloat, ok := claims["exp"].(float64)
if !ok {
    return errors.New("invalid exp claim: must be a number")
}
exp := int64(expFloat)

This avoids subtle bugs where exp is treated as the wrong type.


Struct-Based Claims: A Cleaner Approach

Instead of manually asserting types, you can define a struct that matches your claims:

type CustomClaims struct {
    Sub string `json:"sub"`
    Exp int64  `json:"exp"`
}

var claims CustomClaims
if err := json.Unmarshal(payload, &claims); err != nil {
    return err
}

This approach has several benefits:

  • Stricter validation (invalid types cause errors immediately).
  • Less boilerplate (no repeated type assertions).
  • Clearer code (you know exactly what data your JWT should contain).

Additional Considerations

  1. Leeway for Timestamps
    JWT timestamps like exp and nbf should account for clock skew. Many libraries let you configure a small leeway (e.g., ±30 seconds).
  2. Custom Types
    If your JWT contains structured data (arrays or objects), assert them as []interface{} or map[string]interface{}, then process recursively or map them into a struct.
  3. Error Handling
    Always handle type assertion failures. Ignoring them can lead to runtime panics.
  4. Security
    Don’t just trust that claims are the right type—validate them. An attacker could craft a JWT where exp is a string instead of a number to bypass checks in sloppy code.
  5. Library Choice
    If possible, use a well-established Go JWT library (e.g., golang-jwt/jwt/v5). These libraries already implement strict claim validation, but you should still remain cautious with custom claims.

Nil Values
A missing claim might show up as nil. Always check before asserting:

if claims["role"] == nil {
    return errors.New("role claim missing")
}

Finally

Working with JWTs and JSON in Go requires more explicit handling than in PHP, but that explicitness is a strength. By forcing you to deal with types directly, Go reduces the risk of subtle bugs and increases the reliability of your authentication code.

The key is to always assert and convert types carefully, especially for numbers and strings. When possible, prefer defining struct-based claims over relying on raw maps, and never assume claims are valid just because they exist.

Type safety in Go might feel rigid at first, but once you embrace it, you’ll find it leads to clearer, safer, and more maintainable JWT handling.

Support Us