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 x

In 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" other

The 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    statusOptions

The 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    → 204

Servant 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
FromJSON is the Aeson typeclass for JSON deserialization. Types with a FromJSON instance can be produced from a Value by the parseJSON :: Value -> Parser a method. The Parser monad accumulates failures with path information rather than throwing exceptions. FromJSON instances are typically auto-derived via DeriveGeneric or written manually using withObject, (.:), and (.:?). The instance is used implicitly by decode, eitherDecode, and all web frameworks that integrate with Aeson.
ToJSON
ToJSON is the Aeson typeclass for JSON serialization. It provides two methods: toJSON :: a -> Value (produces an intermediate Value) and toEncoding :: a -> Encoding (streams directly to a builder without an intermediate Value). For high-throughput APIs, implementing toEncoding is important — Warp and Servant use it automatically. The default toEncoding calls toJSON and then serializes the Value, which is correct but slightly slower than a direct implementation using pairs and (.=).
eitherDecode
eitherDecode :: FromJSON a => ByteString -> Either String a is the recommended function for parsing JSON in Haskell. It accepts a lazy ByteString and returns Right value on success or Left errorMessage on 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. Use eitherDecodeStrict for strict ByteString input. The decode variant returns Maybe a and discards the error; prefer eitherDecode in all production code.
DeriveGeneric
DeriveGeneric is a GHC language extension that generates a GHC.Generics.Generic instance for any data type. The Generic instance 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 generate FromJSON and ToJSON instances without Template Haskell or code generation. Enabling DeriveGeneric adds 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.Value is a sum type representing any valid JSON value: Object (HashMap Text Value), Array (Vector Value), String Text, Number Scientific, Bool Bool, and Null. It is the intermediate representation used internally by Aeson between parsing and type-class dispatch. You use Value directly when you need dynamic JSON processing — reading fields without a full type, forwarding JSON blobs, or using lens-aeson for point-free traversal. All types with ToJSON instances can be converted to Value via toJSON, and all types with FromJSON instances can be parsed from a Value via fromJSON.
Options
Data.Aeson.Types.Options is a configuration record that controls how the generic FromJSON and ToJSON instances behave. Key fields include fieldLabelModifier :: String -> String (transforms Haskell field names to JSON keys), constructorTagModifier :: String -> String (transforms constructor names for sum type encoding), omitNothingFields :: Bool (suppress null output for Nothing values), and sumEncoding (controls how sum types are encoded: TaggedObject, ObjectWithSingleField, or TwoElemArray). Pass a customized Options value to genericParseJSON and genericToJSON instead 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