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
- Convert unsupported values to supported primitives before hashing: `SHA256.hexdigest(obj.to_s)` for scalars, or map model objects to Arrays/Hashes of strings.
- For timestamps use an Integer epoch (`time.to_i`) so digests stay stable across runs.
- If you control the helper, extend the case statement with the types you need rather than relying on inspect output.
- 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
- The helper never calls to_s — convert values (Time#to_i, model#slice) before hashing.
- Check nested containers: Hash/Array elements are recursed, so a bad leaf several levels deep is the real culprit.
- Standardize on primitives (String/Symbol/Integer/Float/true/false/nil/Array/Hash) in anything you plan to digest.
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
- commit_oid must be a commit SHA1
- Duplicate language name: #{language.name}
- Duplicate language name: #{language.fs_name}
- Duplicate alias: #{name}
- Extension is missing a '.': #{extension.inspect}
AI-assisted analysis of github-linguist/linguist@b45dbe9b28 (2026-08-21).
Data as JSON: /api/errors/ff28eea7bf3194ff.
Report an issue: GitHub.