Template Haskell to Derive JSON Instances: A Practical Guide
Template Haskell can auto‑generate Aeson JSON instances, cutting boilerplate and reducing errors. See a step‑by‑step example, trade‑offs, and best practices for safe, maintainable usage.
23 Feb 2026, 21:15 UTC

Problem
When a Haskell project grows, each data type that needs to be serialized to JSON requires a ToJSON and FromJSON instance. Writing these by hand is repetitive and error‑prone, especially for large records. Developers often ask: Can we generate these instances automatically?
Thesis
Template Haskell (TH) provides a compile‑time metaprogramming facility that can generate the boilerplate for you. By invoking a helper like deriveJSON from the aeson library, the compiler creates a full instance declaration behind the scenes, eliminating manual code and reducing the chance of typos.
Concrete Worked Example
Below is a minimal module that demonstrates the workflow. The example assumes you have aeson and template-haskell listed as dependencies in package.yaml or cabal.project.local.
{-# LANGUAGE TemplateHaskell #-}
module MyModule where
import Data.Aeson (ToJSON, FromJSON, defaultOptions, deriveJSON)
-- 1. Define a data type
data User = User
{ userId :: Int
, userName :: String
, email :: String
} deriving (Show)
-- 2. Ask TH to generate the instances
$(deriveJSON defaultOptions ''User)
Compile this module with ghc -O2 MyModule.hs or via your build tool. The TH splice $(deriveJSON …) expands into a full instance ToJSON User where … and instance FromJSON User where … block. You can verify the expansion in GHCi:
ghci> :show module MyModule
-- lists the generated code
ghci> :type User
User :: *
ghci> :type toJSON (User 1 "alice" "[contact removed]")
-- shows that toJSON is available
Running a quick round‑trip test is also useful:
import Data.Aeson (encode, decode)
import qualified Data.ByteString.Lazy.Char8 as BL
let user = User 1 "alice" "[contact removed]"
print $ decode (encode user) :: Maybe User
-- Should print: Just (User 1 "alice" "[contact removed]")
Trade‑offs and Limitations
- Compile‑time overhead: Each TH splice triggers the compiler to run the code generator. With dozens of types, build times can grow noticeably.
- Debugging complexity: Errors refer to the expanded code, which can be cryptic. Using
-ddump-spliceshelps inspect the generated code before it is type‑checked. - Readability for newcomers: Generated instances are not visible in the source file, making it harder for a reader to see exactly how fields are mapped.
- Binary bloat: The generated code is part of the compiled module. If many types are derived, the binary can become larger.
Actionable Guidance
- Scope usage: Keep TH derivations in a dedicated module (e.g.,
DeriveJSON.hs) and export only the data types. This isolates the generated code. - Document the splice: Add a comment explaining that the following line is a TH invocation and why it is safe.
- Provide manual fallbacks: For critical types that may evolve, write a hand‑crafted instance to avoid surprises.
- Measure impact: Run
ghc -fforce-recomp -vwith and without the splice to quantify compile time differences. - Use
-ddump-splices: When debugging, compile with this flag to see the exact code the compiler is injecting. - Keep options simple: Start with
defaultOptions. If you need custom field naming or omission rules, pass a modifiedOptionsstructure.
Conclusion
Template Haskell turns repetitive JSON instance boilerplate into a single line of declarative code. The payoff is cleaner source files and fewer typos. However, developers should weigh the compile‑time cost, potential for cryptic errors, and reduced readability. By following the guidelines above—isolating TH usage, documenting splices, and measuring performance—you can harness TH’s power while keeping your codebase maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.