JSON in Haskell: Aeson FromJSON/ToJSON, Generic Derivation & eitherDecode
Last updated:
Haskell handles JSON through the Aeson library — the de-facto standard for JSON encoding and decoding in the Haskell ecosystem. Data.Aeson.decode :: FromJSON a => ByteString -> Maybe a parses JSON safely without exceptions, returning Nothing on failure. eitherDecode :: ByteString -> Either String a is preferred in production — it returns the parse error message as a Left String instead of discarding it. deriving (Generic, FromJSON, ToJSON) generates complete JSON instances for any record type with two pragma lines and a deriving clause — zero manual serialization code. encode :: ToJSON a => a -> ByteString serializes to a lazy ByteString. Aeson uses Text (not String) for JSON string values, keeping everything UTF-8 efficient. Manual instances use object ["field" .= value] for encoding and v .: "field"for decoding. This guide covers Aeson's decode/encode, Generic derivation, eitherDecode, manual FromJSON/ToJSON instances, Value for dynamic JSON, customizing field names with Options, and using Aeson with Servant APIs.
Auto-Deriving FromJSON and ToJSON with DeriveGeneric
The fastest path to Aeson JSON support for a Haskell record type is enabling DeriveGeneric and adding two empty instance declarations. GHC generates the full FromJSON and ToJSON implementations at compile time via GHC.Generics — no Template Haskell, no code generation step, and no runtime overhead beyond what a hand-written instance would produce. The generated instances map Haskell field names to JSON keys exactly, so a field named userName becomes the JSON key "userName".
{-# LANGUAGE DeriveGeneric #-}
module Main where
import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
import Data.Text (Text)
import GHC.Generics (Generic)
-- ── Define a record type ──────────────────────────────────────────
data User = User
{ userId :: Int
, userName :: Text
, userEmail :: Text
, userAdmin :: Bool
} deriving (Show, Eq, Generic)
-- ── Two lines: full FromJSON + ToJSON from the Generic rep ────────
instance FromJSON User
instance ToJSON User
-- Result: User maps to/from
-- { "userId": 1, "userName": "Alice", "userEmail": "alice@example.com", "userAdmin": false }
-- ── Nested records work the same way ─────────────────────────────
data Address = Address
{ street :: Text
, city :: Text
, zip :: Text
} deriving (Show, Eq, Generic)
instance FromJSON Address
instance ToJSON Address
data Person = Person
{ personName :: Text
, personAge :: Int
, personAddress :: Address -- nested record, auto-serialized
} deriving (Show, Eq, Generic)
instance FromJSON Person
instance ToJSON Person
-- ── Maybe fields become optional JSON keys ────────────────────────
data Product = Product
{ productId :: Int
, productName :: Text
, productDesc :: Maybe Text -- absent or null in JSON → Nothing
} deriving (Show, Eq, Generic)
instance FromJSON Product
instance ToJSON Product
-- ── Lists serialize to JSON arrays automatically ──────────────────
data Catalog = Catalog
{ catalogItems :: [Product]
, catalogTotal :: Int
} deriving (Show, Eq, Generic)
instance FromJSON Catalog
instance ToJSON Catalog
-- ── Deriving with DeriveAnyClass (one-liner alternative) ─────────
-- Requires {-# LANGUAGE DeriveAnyClass #-} pragma
-- data Tag = Tag { tagName :: Text, tagCount :: Int }
-- deriving (Show, Generic, FromJSON, ToJSON)The generated instances are indistinguishable from hand-written ones in terms of correctness and performance. Aeson's generic derivation uses the same typeclass machinery that a human would write — it is not a macro that emits slower code. For record types with more than 10 fields, the performance of the derived instance is identical to a manual one because both compile down to the same Core IR. The only case where you must write a manual instance is when the JSON key names differ from the Haskell field names in a non-uniform way, or when the JSON structure does not correspond directly to a record (for example, a sum type with a "type" discriminator field).
decode and eitherDecode: Safe JSON Parsing
Aeson never throws exceptions during parsing. The two primary parse functions differ only in how they surface failures. decode returns Nothing on any error; eitherDecode returns Left errorMessage with a descriptive string. In production code, always prefer eitherDecode — the error message tells you exactly which field failed, what type was expected, and what was found instead, which is essential for debugging API integration issues.
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS -- strict ByteString
-- ── decode: returns Maybe — discards the error ────────────────────
parseUser :: ByteString -> Maybe User
parseUser = decode
-- Usage:
-- decode "{"userId":1,"userName":"Alice","userEmail":"alice@example.com","userAdmin":false}"
-- → Just (User {userId = 1, userName = "Alice", ...})
--
-- decode "{"userId":"not-a-number",...}"
-- → Nothing (error silently discarded)
-- ── eitherDecode: returns Either String — keeps the error ─────────
parseUserSafe :: ByteString -> Either String User
parseUserSafe = eitherDecode
-- Usage:
-- eitherDecode "{"userId":"not-a-number",...}"
-- → Left "Error in $.userId: expected Int, encountered String"
--
-- eitherDecode "{}"
-- → Left "Error in $: key "userId" not found"
-- ── eitherDecodeStrict: for strict ByteString ─────────────────────
-- Use when you already have BS.ByteString in memory (e.g. from a web framework)
parseUserStrict :: BS.ByteString -> Either String User
parseUserStrict = eitherDecodeStrict
-- ── Parsing in IO — reading from a file ──────────────────────────
loadUserFromFile :: FilePath -> IO (Either String User)
loadUserFromFile path = do
bytes <- BL.readFile path
return (eitherDecode bytes)
-- ── Parsing from an HTTP response body ───────────────────────────
-- import Network.HTTP.Client (responseBody)
-- let body = responseBody response :: ByteString
-- case eitherDecode body of
-- Left err -> putStrLn ("Parse error: " <> err)
-- Right user -> putStrLn ("Parsed: " <> show (userName user))
-- ── Parsing a list of values ──────────────────────────────────────
parseUsers :: ByteString -> Either String [User]
parseUsers = eitherDecode
-- ── Type application syntax (GHC 8+) ─────────────────────────────
-- Use @Type annotation instead of :: on the result
-- result = eitherDecode @User bytes
-- result = eitherDecode @[User] bytes
-- ── Handling parse results idiomatically ─────────────────────────
processPayload :: ByteString -> IO ()
processPayload bytes =
case eitherDecode bytes of
Left err -> putStrLn $ "JSON parse error: " <> err
Right user -> putStrLn $ "Welcome, " <> show (userName user)The Either String error from eitherDecode follows the path notation used by Aeson internally: Error in $.users[2].email tells you the exact JSONPath to the failing value. This makes it straightforward to report meaningful errors to API clients or to logging systems. For high-throughput applications where you need to parse thousands of small JSON payloads per second, consider the decodeStrict' variant (with a prime) which forces the result to normal form immediately, preventing lazy thunk buildup.
encode: Serializing Haskell Values to JSON
encode :: ToJSON a => a -> ByteString converts any value with a ToJSON instance to a lazy ByteString of minified JSON. The function is pure and total — it never fails. For pretty-printed output with indentation, use encodePretty from the aeson-pretty package. For maximum throughput in HTTP response bodies, use toEncoding which builds the output incrementally without constructing an intermediate Value.
import Data.Aeson (encode, toJSON, ToJSON)
import Data.Aeson.Encode.Pretty (encodePretty) -- from aeson-pretty
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
-- ── encode: lazy ByteString output ───────────────────────────────
let user = User { userId = 1, userName = "Alice"
, userEmail = "alice@example.com", userAdmin = False }
encode user
-- → "{"userId":1,"userName":"Alice","userEmail":"alice@example.com","userAdmin":false}"
-- Print to stdout (lazy ByteString as UTF-8 text)
BLC.putStrLn (encode user)
-- ── Write JSON to a file ──────────────────────────────────────────
BL.writeFile "user.json" (encode user)
-- ── Pretty print with aeson-pretty ───────────────────────────────
-- encodePretty produces indented, sorted-key output:
-- {
-- "userAdmin": false,
-- "userEmail": "alice@example.com",
-- "userId": 1,
-- "userName": "Alice"
-- }
BLC.putStrLn (encodePretty user)
-- ── toJSON: produce a Value, not ByteString ───────────────────────
-- Useful when you need to inspect or manipulate before serializing
let val = toJSON user -- :: Value
-- val = Object (fromList [("userId", Number 1.0), ("userName", String "Alice"), ...])
-- ── Encoding a list ───────────────────────────────────────────────
let users = [user, user { userId = 2, userName = "Bob" }]
encode users
-- → "[{"userId":1,...},{"userId":2,...}]"
-- ── High-performance encoding with toEncoding ─────────────────────
-- toEncoding avoids the intermediate Value allocation.
-- Servant and Warp use this automatically when the response type is JSON.
-- You rarely need to call it directly.
-- ── Encoding to a strict ByteString ──────────────────────────────
import qualified Data.ByteString as BS
let strict = BL.toStrict (encode user) -- :: BS.ByteString
-- ── Round-trip test ───────────────────────────────────────────────
-- The encode/eitherDecode round-trip should always be the identity:
-- (eitherDecode (encode x) :: Either String User) == Right xIn web server contexts (Warp, Servant, Yesod), you typically never call encode directly — the framework discovers the ToJSON instance and calls toEncoding internally for zero-copy streaming output. Only call encode explicitly when you need the result as a ByteString for logging, file I/O, cache storage, or message queue payloads.
Manual FromJSON and ToJSON Instances
When the auto-derived instances do not match the required JSON shape — because JSON key names differ from Haskell field names, the JSON structure is a tagged union, or you want to add computed fields — write manual instances using the parseJSON, object, .=, .:, and .:?combinators. Manual instances compose cleanly: a nested type's parser is called automatically when the outer type's parser encounters the nested field.
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.Text (Text)
-- ── Manual FromJSON with (.:) and (.:?) ──────────────────────────
data Event = Event
{ eventId :: Int
, eventTitle :: Text
, eventTags :: [Text]
, eventNote :: Maybe Text -- optional field
} deriving (Show)
instance FromJSON Event where
parseJSON = withObject "Event" $ \v -> Event
<$> v .: "id" -- required: fails if absent
<*> v .: "title"
<*> v .: "tags"
<*> v .:? "note" -- optional: Nothing if absent or null
-- JSON: {"id":1,"title":"Launch","tags":["prod","v2"]}
-- → Event {eventId=1, eventTitle="Launch", eventTags=["prod","v2"], eventNote=Nothing}
-- ── (.:?) with default using (.!=) ───────────────────────────────
instance FromJSON Product where
parseJSON = withObject "Product" $ \v -> do
i <- v .: "id"
n <- v .: "name"
desc <- v .:? "description" .!= "No description" -- default value
pure (Product i n (Just desc))
-- ── Manual ToJSON with object and (.=) ───────────────────────────
instance ToJSON Event where
toJSON e = object
[ "id" .= eventId e
, "title" .= eventTitle e
, "tags" .= eventTags e
, "note" .= eventNote e -- Nothing → null in output
]
-- toEncoding (optional, but faster — skips Value allocation):
toEncoding e = pairs
( "id" .= eventId e
<> "title" .= eventTitle e
<> "tags" .= eventTags e
<> "note" .= eventNote e
)
-- ── Tagged union (discriminator "type" field) ─────────────────────
data Shape
= Circle { radius :: Double }
| Rect { width :: Double, height :: Double }
deriving (Show)
instance FromJSON Shape where
parseJSON = withObject "Shape" $ \v -> do
tag <- v .: "type" :: Parser Text
case tag of
"circle" -> Circle <$> v .: "radius"
"rect" -> Rect <$> v .: "width" <*> v .: "height"
_ -> fail $ "Unknown shape type: " <> show tag
instance ToJSON Shape where
toJSON (Circle r) = object ["type" .= ("circle" :: Text), "radius" .= r]
toJSON (Rect w h) = object ["type" .= ("rect" :: Text), "width" .= w, "height" .= h]
-- ── withArray: parse a JSON array manually ────────────────────────
instance FromJSON Event where
parseJSON (Array a) = ... -- process Vector Value
parseJSON (Object v) = ... -- normal object parsing
parseJSON other = typeMismatch "Event" otherThe withObject, withArray, withText, and withBool combinators provide precise type mismatch errors: if you call withObject "Event" ... on a JSON array, Aeson reports Error in $: expected Object, but encountered Array — far more useful than a generic parse failure. The typeMismatch function produces the same style of error for custom pattern-matched parsers. Use fail inside a Parser to signal semantic validation errors (unknown enum value, negative count) with a custom message.
Value: Dynamic JSON Without a Fixed Type
The Value type represents any valid JSON value without committing to a specific Haskell type. It is a sum type with constructors for each JSON kind: Object, Array, String, Number, Bool, and Null. Use Value when you need to pass through JSON without interpreting it, when the JSON structure is heterogeneous, or when you want to extract a small subset of fields from a large payload without defining a type for the whole structure.
import Data.Aeson
import Data.Aeson.Types (parseMaybe)
import qualified Data.HashMap.Strict as HM
import qualified Data.Vector as V
import Data.Text (Text)
-- ── Parse any JSON into a Value ───────────────────────────────────
let raw = "{"name":"Alice","scores":[95,87,100]}"
let val = decode raw :: Maybe Value
-- → Just (Object (fromList [("name", String "Alice"), ("scores", Array [Number 95, ...])]))
-- ── Pattern-match on Value constructors ──────────────────────────
printValue :: Value -> IO ()
printValue (Object obj) = putStrLn $ "Object with " <> show (HM.size obj) <> " keys"
printValue (Array arr) = putStrLn $ "Array with " <> show (V.length arr) <> " elements"
printValue (String t) = putStrLn $ "String: " <> show t
printValue (Number n) = putStrLn $ "Number: " <> show n
printValue (Bool b) = putStrLn $ "Bool: " <> show b
printValue Null = putStrLn "Null"
-- ── Extract a field from an Object value ─────────────────────────
lookupField :: Text -> Value -> Maybe Value
lookupField key (Object obj) = HM.lookup key obj
lookupField _ _ = Nothing
-- lookupField "name" val → Just (String "Alice")
-- ── parseMaybe: apply a Parser to a Value ────────────────────────
extractName :: Value -> Maybe Text
extractName v = parseMaybe (.: "name") =<< case v of
Object obj -> Just obj
_ -> Nothing
-- Or more concisely with withObject:
extractName' :: Value -> Maybe Text
extractName' = parseMaybe (withObject "root" (.: "name"))
-- ── Pass-through JSON: keep a raw Value in a typed record ─────────
data ApiResponse = ApiResponse
{ responseStatus :: Text
, responseData :: Value -- raw JSON blob, not further parsed
} deriving (Show, Generic)
instance FromJSON ApiResponse
instance ToJSON ApiResponse
-- ── lens-aeson: point-free traversal (import lens-aeson package) ──
-- val ^? key "name" . _String → Just "Alice"
-- val ^? key "scores" . nth 0 . _Number → Just 95.0
-- val ^.. key "scores" . values . _Number → [95.0, 87.0, 100.0]Storing a Value field inside a typed record (as in responseData :: Value) is a common pattern for API wrappers that need to forward a JSON payload to another system without inspecting it. The outer envelope is parsed into typed fields, while the opaque payload remains as Value and is re-encoded verbatim when needed. This avoids double-parsing and guarantees that the forwarded JSON is identical to the received JSON.
Customizing Field Names with Options
Haskell convention uses camelCase field names with a record-name prefix (e.g. userName, userAge), while JSON APIs often use camelCase without a prefix or snake_case. Aeson's Options record and genericParseJSON / genericToJSON functions let you transform field names at the instance boundary without touching the data type definition or writing a fully manual instance.
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import Data.Aeson.Types (Options(..))
import Data.Char (toLower, toUpper, isUpper)
import GHC.Generics (Generic)
-- ── Record with prefixed Haskell names ────────────────────────────
data User = User
{ userName :: Text
, userAge :: Int
, userEmail :: Text
} deriving (Show, Generic)
-- ── fieldLabelModifier: strip "user" prefix, lowercase first char ─
userOptions :: Options
userOptions = defaultOptions
{ fieldLabelModifier = s ->
let stripped = drop 4 s -- remove "user" prefix (4 chars)
in case stripped of
[] -> []
(c:cs) -> toLower c : cs
}
-- "userName" → "name", "userAge" → "age", "userEmail" → "email"
instance FromJSON User where
parseJSON = genericParseJSON userOptions
instance ToJSON User where
toJSON = genericToJSON userOptions
toEncoding = genericToEncoding userOptions
-- JSON: {"name":"Alice","age":30,"email":"alice@example.com"}
-- ── snake_case conversion ─────────────────────────────────────────
camelToSnake :: String -> String
camelToSnake [] = []
camelToSnake (c:cs)
| isUpper c = '_' : toLower c : camelToSnake cs
| otherwise = c : camelToSnake cs
snakeOptions :: Options
snakeOptions = defaultOptions
{ fieldLabelModifier = camelToSnake . \(c:cs) -> toLower c : cs }
-- "userName" → "user_name", "userAge" → "user_age"
-- ── omitNothingFields: omit Maybe fields when Nothing ────────────
data Profile = Profile
{ profileName :: Text
, profileBio :: Maybe Text
} deriving (Show, Generic)
profileOptions :: Options
profileOptions = defaultOptions
{ fieldLabelModifier = drop 7 -- strip "profile" prefix
, omitNothingFields = True -- omit bio when Nothing
}
instance ToJSON Profile where
toJSON = genericToJSON profileOptions
-- Profile "Alice" Nothing → {"name":"Alice"} (bio omitted)
-- Profile "Bob" (Just "Writer") → {"name":"Bob","bio":"Writer"}
-- ── Constructor tag modifier for sum types ────────────────────────
data Status = Active | Inactive | Suspended
deriving (Show, Generic)
statusOptions :: Options
statusOptions = defaultOptions
{ constructorTagModifier = map toLower }
-- Active → "active", Inactive → "inactive"
instance FromJSON Status where parseJSON = genericParseJSON statusOptions
instance ToJSON Status where toJSON = genericToJSON statusOptionsThe aeson-casing package provides ready-made fieldLabelModifier functions for the most common naming patterns: camelCase, snakeCase, trainCase (kebab-case), and pascalCase. Import Data.Aeson.Casing and use fieldLabelModifier = snakeCase instead of writing the conversion by hand. The package is compatible with all Aeson versions and adds no runtime overhead — the modifier runs once at construction time, not on every encode/decode call.
Aeson with Servant: Typed JSON HTTP APIs
Servant is a type-level web framework where the API shape is encoded in a Haskell type. JSON encoding and decoding are handled automatically: any endpoint declared with \'[JSON] as the content type uses Aeson's FromJSON for request bodies and ToJSON for response bodies. The handler function receives and returns Haskell values — no manual encode or decode calls in handler code.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
module Api where
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import GHC.Generics (Generic)
import Servant
-- ── Request and response types with Aeson instances ───────────────
data CreateUserReq = CreateUserReq
{ createName :: Text
, createEmail :: Text
} deriving (Show, Generic)
instance FromJSON CreateUserReq -- Aeson decodes the request body
instance ToJSON CreateUserReq
data UserResp = UserResp
{ respId :: Int
, respName :: Text
, respEmail :: Text
} deriving (Show, Generic)
instance FromJSON UserResp -- Aeson encodes the response body
instance ToJSON UserResp
-- ── API type definition ───────────────────────────────────────────
type UserAPI
= "users" :> Get '[JSON] [UserResp]
:<|> "users" :> ReqBody '[JSON] CreateUserReq :> Post '[JSON] UserResp
:<|> "users" :> Capture "id" Int :> Get '[JSON] UserResp
:<|> "users" :> Capture "id" Int :> DeleteNoContent
-- ── Handler implementation ────────────────────────────────────────
-- Servant calls eitherDecode on the request body and encode on the response
-- — you write pure Haskell functions:
listUsers :: Handler [UserResp]
listUsers = pure
[ UserResp 1 "Alice" "alice@example.com"
, UserResp 2 "Bob" "bob@example.com"
]
createUser :: CreateUserReq -> Handler UserResp
createUser req = do
-- Servant has already validated and decoded req via FromJSON
let newId = 42
pure $ UserResp newId (createName req) (createEmail req)
getUser :: Int -> Handler UserResp
getUser uid =
-- Return 404 if not found — Servant encodes the error as JSON automatically
if uid == 1
then pure (UserResp 1 "Alice" "alice@example.com")
else throwError err404
deleteUser :: Int -> Handler NoContent
deleteUser _ = pure NoContent
-- ── Server and Warp integration ───────────────────────────────────
server :: Server UserAPI
server = listUsers :<|> createUser :<|> getUser :<|> deleteUser
userApp :: Application
userApp = serve (Proxy :: Proxy UserAPI) server
-- main :: IO ()
-- main = run 8080 userApp
-- → Listening on port 8080
-- GET /users → 200 [{...},{...}]
-- POST /users → 201 {...}
-- GET /users/1 → 200 {...}
-- GET /users/99 → 404
-- DELETE /users/1 → 204Servant also generates an API client from the same type definition using servant-client. The client functions accept and return the same Haskell types — FromJSON on the client side and ToJSON on the server side — making the JSON encoding contract compile-time verified end-to-end. If a field is added to UserResp without updating both the server handler and the client code, the compiler rejects the program before it reaches production. For OpenAPI documentation, servant-openapi3 generates an OpenAPI 3.x spec automatically from the same API type, deriving JSON schemas from the Aeson instances.
Key Terms
- FromJSON
FromJSONis the Aeson typeclass for JSON deserialization. Types with aFromJSONinstance can be produced from aValueby theparseJSON :: Value -> Parser amethod. TheParsermonad accumulates failures with path information rather than throwing exceptions.FromJSONinstances are typically auto-derived viaDeriveGenericor written manually usingwithObject,(.:), and(.:?). The instance is used implicitly bydecode,eitherDecode, and all web frameworks that integrate with Aeson.- ToJSON
ToJSONis the Aeson typeclass for JSON serialization. It provides two methods:toJSON :: a -> Value(produces an intermediateValue) andtoEncoding :: a -> Encoding(streams directly to a builder without an intermediateValue). For high-throughput APIs, implementingtoEncodingis important — Warp and Servant use it automatically. The defaulttoEncodingcallstoJSONand then serializes theValue, which is correct but slightly slower than a direct implementation usingpairsand(.=).- eitherDecode
eitherDecode :: FromJSON a => ByteString -> Either String ais the recommended function for parsing JSON in Haskell. It accepts a lazyByteStringand returnsRight valueon success orLeft errorMessageon failure. The error message includes the JSONPath to the failing location and a description of the type mismatch or missing key, making it actionable for debugging. UseeitherDecodeStrictfor strictByteStringinput. Thedecodevariant returnsMaybe aand discards the error; prefereitherDecodein all production code.- DeriveGeneric
DeriveGenericis a GHC language extension that generates aGHC.Generics.Genericinstance for any data type. TheGenericinstance encodes the structure of the type (field names, constructor names, arity) as a type-level representation that library code can inspect. Aeson's generic machinery uses this representation to generateFromJSONandToJSONinstances without Template Haskell or code generation. EnablingDeriveGenericadds the pragma{-# LANGUAGE DeriveGeneric #-}to the file and costs nothing at runtime — the Generic instance is used only at instance derivation time, not during parsing or encoding.- Value
Data.Aeson.Valueis a sum type representing any valid JSON value:Object (HashMap Text Value),Array (Vector Value),String Text,Number Scientific,Bool Bool, andNull. It is the intermediate representation used internally by Aeson between parsing and type-class dispatch. You useValuedirectly when you need dynamic JSON processing — reading fields without a full type, forwarding JSON blobs, or usinglens-aesonfor point-free traversal. All types withToJSONinstances can be converted toValueviatoJSON, and all types withFromJSONinstances can be parsed from aValueviafromJSON.- Options
Data.Aeson.Types.Optionsis a configuration record that controls how the genericFromJSONandToJSONinstances behave. Key fields includefieldLabelModifier :: String -> String(transforms Haskell field names to JSON keys),constructorTagModifier :: String -> String(transforms constructor names for sum type encoding),omitNothingFields :: Bool(suppressnulloutput forNothingvalues), andsumEncoding(controls how sum types are encoded:TaggedObject,ObjectWithSingleField, orTwoElemArray). Pass a customizedOptionsvalue togenericParseJSONandgenericToJSONinstead of using the default empty instances.
FAQ
How do I parse JSON in Haskell with Aeson?
Add aeson to your .cabal or package.yaml dependencies, import Data.Aeson, and call eitherDecode on a lazy ByteString. For a type MyType with a FromJSON instance: result <- pure (eitherDecode bytes :: Either String MyType). If parsing succeeds, result is Right myValue; if it fails, result is Left errorMessage with a descriptive string explaining what went wrong. For strict ByteStrings already in memory, use eitherDecodeStrict, which avoids the lazy-ByteString wrapper overhead. The decode function returns Maybe a and discards the error — prefer eitherDecode in production code where you need to log or return parse failures. Aeson parses JSON strings as Data.Text (not String), JSON numbers as Scientific (exact rational), JSON booleans as Bool, and JSON null as Nothing in a Maybe context. Parsing never throws exceptions: the return type forces you to handle failure at the call site with at most 2 lines of case analysis.
How do I auto-derive JSON instances for a Haskell record?
Enable the DeriveGeneric language pragma at the top of your file, import GHC.Generics (Generic) and Data.Aeson (FromJSON, ToJSON), then write data MyRecord = MyRecord {'{ field1 :: Text, field2 :: Int }'} deriving (Show, Generic) followed by two empty instance declarations: instance FromJSON MyRecord and instance ToJSON MyRecord. Those 2-line instances have no method definitions — Aeson generates the full JSON encoder and decoder from the record shape automatically via GHC.Generics. The generated instances map Haskell field names to JSON keys directly, so { "field1": "hello", "field2": 42 } round-trips cleanly. For 50+ record types in a codebase, DeriveGeneric eliminates thousands of lines of manual serialization code. If you enable the DeriveAnyClass extension as well, you can collapse this to a single deriving clause: deriving (Show, Generic, FromJSON, ToJSON).
What is the difference between decode and eitherDecode?
Both functions parse a lazy ByteString into a Haskell value, but they differ in how they report failure. decode :: FromJSON a => ByteString -> Maybe a returns Nothing on any parse error, discarding all information about what went wrong. eitherDecode :: FromJSON a => ByteString -> Either String a returns Left errorMessage with a human-readable description — for example, "Error in $.age: expected Int, encountered String". In production code, always prefer eitherDecode so you can log the error or surface it to users. Use decode only in REPL exploration or tests where you just need a boolean success/failure check. For strict ByteStrings (already fully in memory, as from most web frameworks), use eitherDecodeStrict or decodeStrict respectively — same semantics, different input type.
How do I serialize a Haskell type to JSON?
Call encode :: ToJSON a => a -> ByteString on any value with a ToJSON instance. encode returns a lazy ByteString of minified JSON and is total — it never fails or throws. To pretty-print with 2-space indentation, use encodePretty from the aeson-pretty package: encodePretty myValue. To write JSON to a file: Data.ByteString.Lazy.writeFile "output.json" (encode myValue). To send JSON over HTTP, set the request body to RequestBodyLBS (encode myValue) and the Content-Type header to application/json. For Servant APIs, returning a JSON-encoded value is automatic — declare the endpoint return type as Get \'[JSON] MyType and Servant calls toEncoding for you. The encode function produces compact JSON with no extra whitespace; for the most efficient output in high-throughput servers, Servant uses toEncoding internally, which streams directly without constructing an intermediate Value.
How do I handle optional JSON fields in Aeson?
Wrap the field type in Maybe. A record field of type Maybe Text maps to a JSON key that may be absent or null. In the auto-derived FromJSON instance, an absent key or a null value both produce Nothing; a present string value produces Just "...". In the auto-derived ToJSON instance, a Nothing field is serialized as null by default. To omit Nothing fields from the JSON output entirely — producing a smaller object — use genericToJSON defaultOptions {'{ omitNothingFields = True }'} in your ToJSON instance. For manual instances, use .:? in parseJSON to parse an optional field: field1 <- v .:? "field1", which returns Nothing when the key is absent. Use .!= to supply a default: field2 <- v .:? "field2" .!= 0 produces 0 when "field2" is missing. The pattern covers over 90% of optional-field use cases in practice.
How do I rename JSON fields in Haskell Aeson?
Use Options with fieldLabelModifier to transform Haskell field names to JSON keys at the instance boundary. The most common pattern strips a record-name prefix and lowercases the first character: defaultOptions {'{ fieldLabelModifier = \\s -> let stripped = drop 4 s in toLower (head stripped) : tail stripped }'} converts "userName" to "name" and "userAge" to "age". Pass the Options value to genericParseJSON and genericToJSON instead of using the default empty instances. The aeson-casing package provides ready-made modifiers: snakeCase, camelCase, trainCase — import Data.Aeson.Casing and write fieldLabelModifier = snakeCase. For ad-hoc field renaming with no uniform pattern, write a manual FromJSON instance using v .: "json_key_name" to bind any JSON key to any Haskell field, regardless of naming conventions.
How do I parse dynamic or unknown JSON in Haskell?
Use the Value type from Data.Aeson, which represents any valid JSON value. Value is a sum type: Object (HashMap Text Value), Array (Vector Value), String Text, Number Scientific, Bool Bool, and Null. Parse with decode :: ByteString -> Maybe Value and then pattern-match or use the lens-aeson library for point-free traversal. The parseMaybe and parseEither functions let you apply a custom parser to a Value without committing to a full type: parseMaybe (.: "age") obj gives Maybe Int from an already-parsed Object. Storing a Value field inside a typed record (responseData :: Value) is a common pattern for API wrappers that forward payloads to another system verbatim. With lens-aeson, you can write val ^? key "name" . _String to extract a specific field as Maybe Text — over 3 lines, you can navigate deep into unknown JSON without defining a single type.
How do I use Aeson with a Servant JSON API?
Servant uses Aeson automatically for any endpoint declared with the \'[JSON] content type. Define your API type using ReqBody \'[JSON] InputType and a return type like Get \'[JSON] OutputType. Servant calls eitherDecode on the request body and toEncoding (not just encode) on the return value — handler functions receive and return plain Haskell values. Your InputType needs a FromJSON instance and your OutputType needs a ToJSON instance; both are typically derived with DeriveGeneric in 2 lines. For JSON error responses, use throwError err400 {'{ errBody = encode errorPayload }'} — Servant sets the Content-Type: application/json header automatically when the body is a ByteString. The servant-client package generates a type-safe API client from the same API type, creating a compile-time contract: if the server changes OutputType, client code that uses the old type fails to compile before reaching production.
Further reading and primary sources
- Aeson Documentation (Hackage) — Official Haskell Aeson package documentation: all functions, instances, and Options fields
- Aeson GitHub Repository — Source code, changelog, and community issues for Aeson
- Real World Haskell — Chapter 16: JSON — Practical guide to JSON processing in real Haskell applications
- lens-aeson on Hackage — Lens-based traversal of Aeson Value trees for dynamic JSON processing
- Servant Tutorial: JSON APIs — Official Servant tutorial covering JSON endpoint types, request body parsing, and response encoding