Serializing JSON in Haskell by Horrifically Abusing the Show Typeclass

Posted on September 21, 2025

(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
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.pack

Usage:

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 xs

Now:

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.