Two Haskell Packages — Which to Use
Haskell has two active Base64 packages on Hackage:
- base64-bytestring — the classic, widely-depended-upon package. Strict and lazy ByteString APIs, URL-safe variant in a submodule, lenient decoder for broken input. Used by
aeson,http-client,jose, and most of the Haskell ecosystem. - base64 (by Emily Pillmore) — a newer, type-safe wrapper that distinguishes padded vs unpadded and standard vs URL-safe at the TYPE level. Supports
Textdirectly. Cleaner API for new code, but the ecosystem leans onbase64-bytestringso you may end up with both in your dependency graph.
For most projects, base64-bytestring is the right default. Switch tobase64 when you want Text support without manualencodeUtf8 calls, or when you want the compiler to enforce padding correctness.
The Core Pattern — base64-bytestring
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS8
import Data.ByteString (ByteString)
-- Encode: ByteString -> ByteString
encoded :: ByteString
encoded = B64.encode "Hello"
-- encoded == "SGVsbG8="
-- Decode: ByteString -> Either String ByteString
decoded :: Either String ByteString
decoded = B64.decode "SGVsbG8="
-- decoded == Right "Hello"
main :: IO ()
main = BS8.putStrLn encodedWorking with Text (UTF-8)
Haskell String is [Char] — a list of characters, NOT a sequence of bytes. Base64 needs bytes. The idiomatic path for text data: Text → encodeUtf8 → ByteString → B64.encode.
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64 as B64
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
encodeText :: T.Text -> ByteString
encodeText = B64.encode . TE.encodeUtf8
-- Example
encodeText "Hello 世界 👋"
-- "SGVsbG8g5LiW55WMIPCfkYs="
-- Round-trip with error handling
roundTrip :: T.Text -> Either String T.Text
roundTrip t = do
decoded <- B64.decode (B64.encode (TE.encodeUtf8 t))
pure (TE.decodeUtf8 decoded)Using the Newer base64 Package
{-# LANGUAGE OverloadedStrings #-}
-- cabal: build-depends: base64
import qualified Data.Text.Encoding.Base64 as T64
import qualified Data.Text as T
-- Direct Text support — no manual encodeUtf8 needed
encoded :: T.Text
encoded = T64.encodeBase64 "Hello 世界"
-- "SGVsbG8g5LiW55WM"
-- URL-safe + unpadded (JWT style)
encodedJWT :: T.Text
encodedJWT = T64.encodeBase64UrlUnpadded "Hello 世界"
-- Decode returns Either T.Text T.Text (left is the error message)
decoded :: Either T.Text T.Text
decoded = T64.decodeBase64 "SGVsbG8g5LiW55WM"URL-Safe Base64 for JWTs
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64.URL as B64Url
import qualified Data.ByteString.Char8 as BS8
-- Standard URL-safe (padding kept)
standardUrl :: ByteString
standardUrl = B64Url.encode (BS8.pack "\251\255")
-- "-_8="
-- JWT style — strip padding manually
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
jwtEncode :: ByteString -> ByteString
jwtEncode = BS8.takeWhile (/= '=') . B64Url.encode
-- Decode with padding re-added
jwtDecode :: ByteString -> Either String ByteString
jwtDecode bs = B64Url.decode (bs <> padding)
where
padding = BS8.replicate (negate (BS8.length bs) `mod` 4) '='Streaming Large Files with Lazy ByteString
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64.Lazy as B64L
import qualified Data.ByteString.Lazy as BSL
-- Encode a file lazily — Haskell streams chunks under the hood
encodeFile :: FilePath -> FilePath -> IO ()
encodeFile inPath outPath = do
contents <- BSL.readFile inPath
BSL.writeFile outPath (B64L.encode contents)
-- Decode a huge Base64 file back to raw bytes
decodeFile :: FilePath -> FilePath -> IO (Either String ())
decodeFile inPath outPath = do
contents <- BSL.readFile inPath
case B64L.decode contents of
Left err -> pure (Left err)
Right bytes -> Right <$> BSL.writeFile outPath bytesHTTP Basic Auth Header
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)
basicAuthHeader :: ByteString -> ByteString -> (ByteString, ByteString)
basicAuthHeader user pass =
("Authorization", "Basic " <> B64.encode (user <> ":" <> pass))
-- Use with http-client
import Network.HTTP.Simple
makeRequest :: IO ()
makeRequest = do
let (name, value) = basicAuthHeader "admin" "secret123"
response <- httpLBS $ setRequestHeader name [value]
$ "https://api.example.com/me"
print (getResponseStatus response)Common Mistakes
- Using
Data.ByteString.packon a String. It takes[Word8], not[Char]. ForString, useData.ByteString.Char8.pack(truncates to Latin-1) or convert viaTextwithencodeUtf8. - Expecting Base64 to accept String directly. It accepts ByteString. If you pass a
String, you'll get a type error. Always encode viaChar8.packorencodeUtf8 . pack. - Using
decodeLenientin production. It silently accepts garbage input by stripping invalid characters. Usedecodeand handle theEitherto get proper error reporting. - Mixing strict and lazy ByteString.
Data.ByteString.Base64andData.ByteString.Base64.Lazyoperate on different types. Pick one for your pipeline and stay consistent, or useData.ByteString.Lazy.toStrict/fromStrictat the boundary. - Forgetting
OverloadedStrings. Without it, string literals in your source are[Char]and the type checker will reject passing them toB64.encode. Enable the pragma at the top of every module that touches ByteString literals.
cabal / stack Setup
-- myproject.cabal
library
build-depends:
base ^>= 4.17
, bytestring ^>= 0.11
, base64-bytestring ^>= 1.2
-- Optional (newer typed API):
, base64 ^>= 0.4
, text ^>= 2.0
-- Then: cabal build (or stack build)GHCi One-Liners
$ cabal repl
ghci> :set -XOverloadedStrings
ghci> import qualified Data.ByteString.Base64 as B64
ghci> B64.encode "Hello"
"SGVsbG8="
ghci> B64.decode "SGVsbG8="
Right "Hello"
-- Or via runghc:
$ echo '{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS8
main = BS8.putStrLn (B64.encode "Hello")' | runghcKey Facts
- Classic package:
- base64-bytestring (ByteString only)
- Modern package:
- base64 (ByteString + Text, typed padding)
- Standard module:
- Data.ByteString.Base64
- URL-safe module:
- Data.ByteString.Base64.URL
- Lazy variant:
- Data.ByteString.Base64.Lazy (streaming large files)
- Encode signature:
- encode :: ByteString -> ByteString
- Decode signature:
- decode :: ByteString -> Either String ByteString
Related Base64 Tools
- Base64 Encode Online — general-purpose encoder
- Base64 Encode in Elixir — another functional-language option
- Base64 Encode in Rust — closest systems-level analog
- URL-Safe Base64 — the JWT variant
- Base64 Decode Online — reverse the encoding
- JWT Debugger — inspect JWT tokens