(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 Show }
A 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
= T.pack
evilJson . replace "True" "true"
. replace "False" "false"
. replace "Nothing" "null"
. replace "[" "{"
. replace "]" "}"
. replace "fromList " ""
. show
where
= T.unpack . T.replace (T.pack old) (T.pack new) . T.pack replace old new
Usage:
> evilJson (Creature "Axolotl" 3 True Nothing)
ghci"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
= case dropWhile (== ' ') s of
fixRecord s '{':rest -> "{" ++ intercalate "," (map fixField (splitOn ',' (init rest))) ++ "}"
-> fixValue s
_
= case break (== '=') f of
fixField f '=':val) -> "\"" ++ trim lbl ++ "\":" ++ fixValue (trim val)
(lbl, -> error "kaboom"
_
= case trim v of
fixValue v "True" -> "true"
"False" -> "false"
"Nothing"-> "null"
'"':xs -> show (takeWhile (/= '"') xs)
'{':_ -> fixRecord v
'[':_ -> fixRecord v
-> if all isDigit xs then xs else show xs xs
Now:
> evilJson (Creature "Axolotl" 3 True Nothing)
ghci"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.