ory/hydra · error

unable to scan type %T as JSON into %T

Error message

unable to scan type %T as JSON into %T

What it means

JSONScan scans a database value (string, []byte, or null) into a destination pointer via json.Unmarshal. It throws this error when the value passed by the database driver is of an unsupported Go type (e.g. int, time.Time, []interface{}), because it cannot convert it to raw JSON text to unmarshal. It indicates a driver/type mismatch rather than invalid JSON.

Source

Thrown at oryx/sqlxx/types.go:501

// UnmarshalJSON sets *m to a copy of data.
func (m *NullJSONObject) UnmarshalJSON(data []byte) error {
	return (*NullJSONRawMessage)(m).UnmarshalJSON(data)
}

// JSONScan is a generic helper for retrieving a SQL JSON-encoded value.
func JSONScan(dst, value any) error {
	// Note: raw is a string (not []byte) because the MySQL driver reuses byte slices across scans.
	// Using strings avoids the need to manually copy the byte slice.
	var raw string
	switch v := value.(type) {
	case nil:
		raw = "null"
	case string:
		raw = v
	case []byte:
		raw = string(v)
	default:
		return fmt.Errorf("unable to scan type %T as JSON into %T", value, dst)
	}
	if err := json.Unmarshal([]byte(raw), dst); err != nil {
		return fmt.Errorf("unable to decode JSON payload into %T: %w", dst, err)
	}
	return nil
}

// NullInt64 represents an int64 that may be null.
// swagger:type int64
// swagger:model nullInt64
type NullInt64 struct {
	Int   int64
	Valid bool // Valid is true if Duration is not NULL
}

// Scan implements the Scanner interface.
func (ns *NullInt64) Scan(value interface{}) error {
	d := sql.NullInt64{}

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Check the actual Go type the driver returns for the column (log %T of the value) and pick a driver setting that returns strings/[]byte for JSON columns
  2. Ensure the DB column is a text/JSON type that the driver delivers as string or []byte
  3. Implement a custom Scan on the destination type that handles the driver's concrete type (e.g. convert numbers/bools with json.Marshal before unmarshal)
  4. Do not bind JSONScan-backed types to columns that are not JSON/text

Example fix

// before: driver returns map for JSONB column
func (j *JSONB) Scan(value interface{}) error { return JSONScan(value, j) }
// after: normalize unsupported types first
func (j *JSONB) Scan(value interface{}) error {
    switch v := value.(type) {
    case []byte, string, nil:
        return JSONScan(value, j)
    default:
        b, err := json.Marshal(value)
        if err != nil { return err }
        return JSONScan(b, j)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

func canJSONScan(value interface{}) bool {
    switch value.(type) {
    case nil, string, []byte:
        return true
    default:
        return false
    }
}

Type guard

func isJSONScanable(v interface{}) bool {
    switch v.(type) {
    case nil, string, []byte:
        return true
    }
    return false
}

Prevention

When it happens

Trigger: Scanning a column whose driver returns a native non-string type (e.g. a JSONB returned as a parsed value, an int, or time.Time) into a field whose Scan delegates to JSONScan (e.g. a JSON/JSONB or nullable JSON column mapped to a custom type wrapping this helper).

Common situations: Using a driver that decodes JSON columns into map[string]interface{} instead of []byte (e.g. certain pq/pgx or sqlite drivers), storing JSON in a non-text column type, or ORM scanning a numeric/boolean column into a JSON-typed struct field.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/8b44caf60172ab37. Report an issue: GitHub.