github-linguist/linguist · error · TypeError

can't convert #{obj.inspect} into String

Error message

can't convert #{obj.inspect} into String

What it means

Linguist::SHA256.hexdigest builds a stable content digest by case-dispatching on the object's type: String, Symbol, Integer, Float, the singleton classes true/false/nil, Array (recursed per element), and Hash (each pair sorted then digested). Any other leaf type falls into the else branch and raises TypeError with "can't convert <inspect> into String" — there is no to_s coercion. The digest is used for object comparison/cache keys, so unsupported values must be converted to primitives by the caller before hashing.

Source

Thrown at lib/linguist/sha256.rb:32

      case obj
      when String, Symbol, Integer, Float
        digest.update "#{obj.class}"
        digest.update "#{obj}"
      when TrueClass, FalseClass, NilClass
        digest.update "#{obj.class}"
      when Array
        digest.update "#{obj.class}"
        for e in obj
          digest.update(hexdigest(e))
        end
      when Hash
        digest.update "#{obj.class}"
        for e in obj.map { |(k, v)| hexdigest([k, v]) }.sort
          digest.update(e)
        end
      else
        raise TypeError, "can't convert #{obj.inspect} into String"
      end

      digest.hexdigest
    end
  end
end

View on GitHub (pinned to b45dbe9b28)

Solutions

  1. Convert unsupported values to supported primitives before hashing: `SHA256.hexdigest(obj.to_s)` for scalars, or map model objects to Arrays/Hashes of strings.
  2. For timestamps use an Integer epoch (`time.to_i`) so digests stay stable across runs.
  3. If you control the helper, extend the case statement with the types you need rather than relying on inspect output.
  4. Check nested Hash/Array contents — the raise often comes from a leaf several levels deep, and obj.inspect in the message tells you which value it was.

Example fix

# before
Linguist::SHA256.hexdigest(user: user, last_login: user.last_login)
# => TypeError: can't convert #<User ...> into String

# after
Linguist::SHA256.hexdigest(user: user.slice(:id, :login), last_login: user.last_login.to_i)
Defensive patterns

Strategy: type-guard

Validate before calling

def sha256_digestable?(obj)
  case obj
  when String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass then true
  when Array then obj.all? { |e| sha256_digestable?(e) }
  when Hash then obj.all? { |k, v| sha256_digestable?(k) && sha256_digestable?(v) }
  else false
  end
end

payload = { id: 1, at: Time.now.to_i } unless sha256_digestable?(payload)

Type guard

def sha256_digestable?(obj)
  case obj
  when String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass then true
  when Array then obj.all? { |e| sha256_digestable?(e) }
  when Hash then obj.all? { |k, v| sha256_digestable?(k) && sha256_digestable?(v) }
  else false
  end
end

Try / catch

begin
  Linguist::SHA256.hexdigest(payload)
rescue TypeError => e
  raise unless e.message =~ /can't convert .* into String/
  Linguist::SHA256.hexdigest(payload.to_s) # last-resort lossy fallback
end

Prevention

When it happens

Trigger: 1) `Linguist::SHA256.hexdigest(Time.now)` — Time is not in the case list. 2) Passing Struct, OpenStruct, Date, BigDecimal, or any custom class instance. 3) A Hash whose keys or values (at any nesting depth) contain unsupported objects — recursion eventually reaches the else branch. 4) Passing a Class/Module object itself.

Common situations: Using the helper to fingerprint payloads that include timestamps or model objects; feeding ActiveRecord/Struct attributes straight in; assuming the method calls to_s like Digest does — it intentionally does not, to keep digests type-stable.

Related errors


AI-assisted analysis of github-linguist/linguist@b45dbe9b28 (2026-08-21). Data as JSON: /api/errors/ff28eea7bf3194ff. Report an issue: GitHub.