(A post for anyone who ever looked at show and thought “close enough.”)
Haskell has a perfectly respectable way to spit out JSON: import aeson, derive some generics, and you’re done.
But where’s the fun in that?
Today we’ll take the path of maximal resistance: we’ll derive JSON straight from Show, no libraries, no types, no mercy.
Bring popcorn, we’re about to commit crimes against computer science.
Embracing the Madness
The Show type-class was never meant for serialization. It was meant for debugging, those pretty-printed records that look almost like JSON… if you squint… after three coffees… and a mild concussion.
Example:
data Creature = Creature
{ name :: String
, age :: Int
, cute :: Bool
, secret :: Maybe String
} deriving ShowA quick show gives:
Creature {name = "Axolotl", age = 3, cute = True, secret = Nothing}Looks kinda like JSON, right?
All we need is a little search-and-replace surgery and, boom, valid JSON. What could possibly go wrong?
The 5-Minute “Serializer”
{-# LANGUAGE OverloadedStrings #-}
module EvilJson where
import qualified Data.Text as T
evilJson :: Show a => a -> T.Text
evilJson = T.pack
. replace "True" "true"
. replace "False" "false"
. replace "Nothing" "null"
. replace "[" "{"
. replace "]" "}"
. replace "fromList " ""
. show
where
replace old new = T.unpack . T.replace (T.pack old) (T.pack new) . T.packUsage:
ghci> evilJson (Creature "Axolotl" 3 True Nothing)
"Creature {name = \"Axolotl\", age = 3, cute = true, secret = null}"Beautiful… ly broken.
Field names aren’t quoted, commas are missing, and the constructor tag is still hanging around like a drunk party guest.
Fine, We’ll Make It Valid
Let’s parse the Show output just enough to fix braces, commas, and quotes, still no JSON library in sight.
fixRecord :: String -> String
fixRecord s = case dropWhile (== ' ') s of
'{':rest -> "{" ++ intercalate "," (map fixField (splitOn ',' (init rest))) ++ "}"
_ -> fixValue s
fixField f = case break (== '=') f of
(lbl, '=':val) -> "\"" ++ trim lbl ++ "\":" ++ fixValue (trim val)
_ -> error "kaboom"
fixValue v = case trim v of
"True" -> "true"
"False" -> "false"
"Nothing"-> "null"
'"':xs -> show (takeWhile (/= '"') xs)
'{':_ -> fixRecord v
'[':_ -> fixRecord v
xs -> if all isDigit xs then xs else show xsNow:
ghci> evilJson (Creature "Axolotl" 3 True Nothing)
{"name":"Axolotl","age":3,"cute":true,"secret":null}Valid JSON, delivered by a chain of String-mangling atrocities.
We have successfully weaponized Show.
Celebrate with a Benchmark
| Metric | aeson | Show-Hack |
|---|---|---|
| Correctness | ✅ | ✅ (for the 3 values we tested) |
| Dependencies | 1 | 0 |
| compile-time | 0.2 s | 0.2 s |
| run-time | fast | O(n²) and proud |
| maintainability | high | negative |
| street cred | meh | legendary |
Moral of the Story
Just because you can derive JSON from Show doesn’t mean you should.
But every now and then, stretching the language until it screams is a great way to learn how things really work, and to appreciate the libraries that save us from ourselves.
Now, please, go install aeson before someone sees this post and revokes your Haskell license.