Haskell JSON with Aeson: FromJSON, ToJSON, Generic Derivation & Servant

Last updated:

Haskell's standard JSON library is Aeson — derive FromJSON and ToJSON instances automatically with {-# LANGUAGE DeriveGeneric #-} and deriving (Generic, FromJSON, ToJSON), then decode with decode :: FromJSON a => ByteString -> Maybe a. decode returns Maybe a (Nothing on parse failure); eitherDecode returns Either String a with an error message — prefer eitherDecode for production code. Aeson uses lazy ByteString internally; Data.Aeson.encode produces a lazy ByteString that you can convert with Data.ByteString.Lazy.toStrict. This guide covers Generic-derived instances, custom parseJSON and toJSON implementations, field name mapping with defaultOptions { fieldLabelModifier }, the Value type for dynamic JSON, Lens-based traversal with lens-aeson, and Servant JSON API endpoints.

Generic Derivation: FromJSON and ToJSON with DeriveGeneric

The fastest path to Aeson JSON support is Generic derivation — enable DeriveGeneric, derive Generic alongside FromJSON and ToJSON, and Aeson generates complete instances at compile time. No boilerplate, no hand-written parsers. GHC.Generics provides a type-level description of your data type that Aeson traverses to produce JSON keys from record field names and JSON arrays from positional constructors.

{-# LANGUAGE DeriveGeneric #-}

module Main where

import Data.Aeson   (FromJSON, ToJSON, decode, encode, eitherDecode)
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy as BL

-- Record type: field names become JSON keys
data User = User
  { userId   :: Int
  , userName :: String
  , userEmail :: String
  , userActive :: Bool
  } deriving (Show, Generic, FromJSON, ToJSON)

-- Encode a User to JSON
main :: IO ()
main = do
  let alice = User { userId = 1, userName = "Alice", userEmail = "alice@example.com", userActive = True }

  -- Encode: Haskell -> JSON ByteString
  let encoded = encode alice
  -- {"userId":1,"userName":"Alice","userEmail":"alice@example.com","userActive":true}
  BL.putStrLn encoded

  -- Decode: JSON ByteString -> Maybe User
  let json = "{\"userId\":2,\"userName\":\"Bob\",\"userEmail\":\"bob@example.com\",\"userActive\":false}"
  case decode json :: Maybe User of
    Nothing   -> putStrLn "Parse failed"
    Just user -> print user

  -- eitherDecode: JSON ByteString -> Either String User
  case eitherDecode json :: Either String User of
    Left  err  -> putStrLn ("Parse error: " ++ err)
    Right user -> print user

-- Nested types: derive Generic on each type
data Address = Address
  { street :: String
  , city   :: String
  , zip    :: String
  } deriving (Show, Generic, FromJSON, ToJSON)

data UserWithAddress = UserWithAddress
  { uwaUser    :: User
  , uwaAddress :: Address
  } deriving (Show, Generic, FromJSON, ToJSON)

-- Sum types: constructor name becomes a "tag" key by default
data Shape
  = Circle  { radius :: Double }
  | Rect    { width :: Double, height :: Double }
  deriving (Show, Generic, FromJSON, ToJSON)
-- encode (Circle 3.0) => {"tag":"Circle","radius":3.0}
-- encode (Rect 4.0 5.0) => {"tag":"Rect","width":4.0,"height":5.0}

Generic derivation works for record types, sum types, and newtypes. For records, every field name becomes a JSON key. For sum types (constructors with multiple cases), Aeson adds a "tag" key with the constructor name by default — this behavior is configurable via sumEncoding in Options. For newtypes wrapping a single value, the derived instance is a transparent passthrough: newtype UserId = UserId Int deriving (Generic, FromJSON, ToJSON) encodes as a plain JSON number. Mutual recursion and phantom types require no special handling beyond deriving Generic on each type involved.

decode and eitherDecode: Parsing JSON Safely

Aeson offers two families of decode functions: decode/decodeStrict returning Maybe a, and eitherDecode/eitherDecodeStrict returning Either String a. The Strict variants accept Data.ByteString.ByteString (strict) instead of Data.ByteString.Lazy.ByteString (lazy). Always use eitherDecode in production — the error string contains a JSONPath location and a description of the mismatch.

import Data.Aeson
import qualified Data.ByteString.Lazy  as BL
import qualified Data.ByteString       as BS

-- decode :: FromJSON a => BL.ByteString -> Maybe a
-- eitherDecode :: FromJSON a => BL.ByteString -> Either String a

validJson :: BL.ByteString
validJson = "{\"userId\":1,\"userName\":\"Alice\",\"userEmail\":\"a@x.com\",\"userActive\":true}"

badJson :: BL.ByteString
badJson = "{\"userId\":\"not-a-number\",\"userName\":\"Alice\",...}"

missingField :: BL.ByteString
missingField = "{\"userId\":1,\"userName\":\"Alice\"}"

-- decode returns Nothing on any failure
decodeExample :: IO ()
decodeExample = do
  print (decode validJson   :: Maybe User)   -- Just (User {userId = 1, ...})
  print (decode badJson     :: Maybe User)   -- Nothing
  print (decode missingField :: Maybe User)  -- Nothing (missing fields)

-- eitherDecode returns descriptive errors
eitherDecodeExample :: IO ()
eitherDecodeExample = do
  -- Success
  print (eitherDecode validJson :: Either String User)
  -- Right (User {userId = 1, userName = "Alice", ...})

  -- Type mismatch
  print (eitherDecode badJson :: Either String User)
  -- Left "Error in $.userId: parsing Int failed, expected Number, but encountered String"

  -- Missing required field
  print (eitherDecode missingField :: Either String User)
  -- Left "Error in $: key \"userEmail\" not found"

-- Strict ByteString variants (no lazy conversion needed)
decodeStrictExample :: BS.ByteString -> IO ()
decodeStrictExample strictBytes = do
  case eitherDecodeStrict strictBytes :: Either String User of
    Left  err  -> putStrLn ("JSON parse error: " ++ err)
    Right user -> print user

-- Decode a JSON array
decodeList :: IO ()
decodeList = do
  let jsonArray = "[{\"userId\":1,...},{\"userId\":2,...}]"
  case eitherDecode jsonArray :: Either String [User] of
    Left  err   -> putStrLn err
    Right users -> mapM_ print users

-- Decode into a generic Value when the schema is unknown
decodeValue :: IO ()
decodeValue = do
  case eitherDecode validJson :: Either String Value of
    Left  err -> putStrLn err
    Right val -> print val  -- Object (fromList [("userId", Number 1.0), ...])

The error messages from eitherDecode use JSONPath-style notation: Error in $.user.age tells you exactly which field failed and at what nesting level. This is invaluable when debugging complex nested structures — Maybe-based decoding gives you no information about whether the parse failed due to a missing key, a type mismatch, or malformed JSON. In HTTP handlers, map Left err to a 400 response that includes the error string in the response body for client-side debugging.

Custom parseJSON and toJSON Implementations

When Generic derivation does not match your JSON schema — mismatched key names, computed fields, conditional encoding, validation logic, or complex sum type layouts — write custom FromJSON and ToJSON instances manually. Aeson's Parser monad and object/(.=)/(.:) combinators make this concise and composable.

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Aeson.Types (prependFailure, typeMismatch)
import Control.Monad    (when)

data Product = Product
  { productId    :: Int
  , productName  :: String
  , priceInCents :: Int       -- stored as cents, JSON as dollars
  , productTags  :: [String]
  } deriving Show

-- Custom ToJSON: encode priceInCents as dollars float
instance ToJSON Product where
  toJSON p = object
    [ "id"    .= productId p
    , "name"  .= productName p
    , "price" .= (fromIntegral (priceInCents p) / 100.0 :: Double)
    , "tags"  .= productTags p
    ]

-- Custom FromJSON: parse dollars float back to cents Int
-- Also validate: price must be non-negative
instance FromJSON Product where
  parseJSON = withObject "Product" $ \v -> do
    pid    <- v .: "id"
    name   <- v .: "name"
    price  <- v .: "price"     -- parsed as Double (dollars)
    tags   <- v .:? "tags" .!= []   -- optional field, default []
    when (price < 0) $
      fail ("price must be non-negative, got: " ++ show (price :: Double))
    pure Product
      { productId    = pid
      , productName  = name
      , priceInCents = round (price * 100)
      , productTags  = tags
      }

-- withObject / withArray / withText / withBool / withScientific
-- are helpers that emit a type-mismatch error automatically
-- if the Value is not the expected JSON type

-- Sum type with custom encoding (not using Generic tag)
data EventType
  = UserCreated { email :: String }
  | OrderPlaced { orderId :: Int, total :: Double }
  deriving Show

instance ToJSON EventType where
  toJSON (UserCreated e) = object ["type" .= ("user_created" :: String), "email" .= e]
  toJSON (OrderPlaced oid t) = object
    [ "type"     .= ("order_placed" :: String)
    , "order_id" .= oid
    , "total"    .= t
    ]

instance FromJSON EventType where
  parseJSON = withObject "EventType" $ \v -> do
    typ <- v .: "type" :: Parser String
    case typ of
      "user_created" -> UserCreated <$> v .: "email"
      "order_placed" -> OrderPlaced <$> v .: "order_id" <*> v .: "total"
      other -> fail ("unknown event type: " ++ other)

-- (.:?) for optional fields (returns Maybe)
-- (.!=) to provide a default when field is missing
-- (<|>) to try alternative parsers
data Config = Config
  { timeout :: Int
  , retries :: Int
  , verbose :: Bool
  } deriving Show

instance FromJSON Config where
  parseJSON = withObject "Config" $ \v -> Config
    <$> v .:? "timeout" .!= 30
    <*> v .:? "retries" .!= 3
    <*> v .:? "verbose" .!= False

The (.:) operator parses a required field and fails with a clear error if the key is missing or the value has the wrong type. (.:?) parses an optional field, returning Nothing if the key is absent. Chaining (.!=) after (.:?) provides a default value, avoiding Maybe in the final record. Use fail inside the Parser monad to emit validation errors — Aeson prepends the field path automatically, so fail "must be positive" becomes Error in $.price: must be positive in the final error message.

Field Name Mapping with defaultOptions

Generic derivation uses Haskell record field names as JSON keys verbatim. Production APIs typically require snake_case JSON keys while idiomatic Haskell uses camelCase record names — or a leading underscore prefix for lens-compatible field names. Override this with genericParseJSON and genericToJSON using a custom Optionsvalue, keeping derivation's benefits while controlling the JSON shape.

{-# LANGUAGE DeriveGeneric #-}

import Data.Aeson
import Data.Aeson.Types (Options(..), defaultOptions, camelTo2)
import GHC.Generics     (Generic)
import Data.Char        (toLower)

-- ── Pattern 1: strip leading underscore (lens-friendly record fields)
data Person = Person
  { _personId    :: Int
  , _personName  :: String
  , _personEmail :: String
  } deriving (Show, Generic)

-- drop 1 removes the leading '_'
personOptions :: Options
personOptions = defaultOptions { fieldLabelModifier = drop 1 }

instance FromJSON Person where
  parseJSON = genericParseJSON personOptions

instance ToJSON Person where
  toJSON     = genericToJSON     personOptions
  toEncoding = genericToEncoding personOptions  -- more efficient than toJSON

-- encode (Person 1 "Alice" "a@x.com")
-- => {"personId":1,"personName":"Alice","personEmail":"alice@x.com"}
-- (leading _ stripped, camelCase preserved)

-- ── Pattern 2: camelCase -> snake_case using camelTo2
data OrderLine = OrderLine
  { orderLineId    :: Int
  , productSku     :: String
  , quantityOrdered :: Int
  , unitPriceUsd   :: Double
  } deriving (Show, Generic)

snakeCaseOptions :: Options
snakeCaseOptions = defaultOptions { fieldLabelModifier = camelTo2 '_' }

instance FromJSON OrderLine where
  parseJSON = genericParseJSON snakeCaseOptions

instance ToJSON OrderLine where
  toJSON     = genericToJSON     snakeCaseOptions
  toEncoding = genericToEncoding snakeCaseOptions

-- encode (OrderLine 5 "SKU-001" 3 19.99)
-- => {"order_line_id":5,"product_sku":"SKU-001","quantity_ordered":3,"unit_price_usd":19.99}

-- ── Pattern 3: strip prefix + snake_case (common library convention)
stripPrefixAndSnake :: String -> String -> String
stripPrefixAndSnake prefix s =
  camelTo2 '_' $ drop (length prefix) s

data ApiResponse = ApiResponse
  { apiResponseStatus  :: String
  , apiResponsePayload :: Value
  , apiResponseMeta    :: Maybe Value
  } deriving (Show, Generic)

apiOptions :: Options
apiOptions = defaultOptions
  { fieldLabelModifier = stripPrefixAndSnake "apiResponse"
  , omitNothingFields  = True   -- don't include null keys for Nothing
  }

instance FromJSON ApiResponse where
  parseJSON = genericParseJSON apiOptions

instance ToJSON ApiResponse where
  toJSON     = genericToJSON     apiOptions
  toEncoding = genericToEncoding apiOptions
-- omitNothingFields: if apiResponseMeta is Nothing, "meta" key is absent from JSON

-- ── toEncoding vs toJSON ──────────────────────────────────────────
-- toEncoding writes directly to the Builder (more efficient, avoids
-- constructing intermediate Value). Always define toEncoding alongside
-- toJSON when implementing instances for performance-sensitive paths.

The omitNothingFields = True option in Options suppresses JSON keys whose Haskell value is Nothing — producing compact JSON without explicit null values for optional fields. This is important for JSON API design where clients distinguish between a missing key and an explicit null. Always implement toEncoding alongside toJSON using genericToEncoding — it writes directly to Aeson's internal Builder without constructing an intermediate Value tree, which measurably improves JSON performance for large payloads.

Value Type: Dynamic JSON Without Fixed Schema

Data.Aeson.Value is a sum type that represents any valid JSON value without a fixed schema — parse unknown JSON structures, build JSON dynamically, or inspect JSON before deciding how to decode it. Value is also the intermediate representation inside Aeson: toJSON produces a Value and parseJSON consumes one.

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM
import qualified Data.Vector         as V
import Data.Text (Text)

-- Value constructors:
--   Object :: Object       (HashMap Text Value)
--   Array  :: Array        (Vector Value)
--   String :: Text   -> Value
--   Number :: Scientific -> Value
--   Bool   :: Bool   -> Value
--   Null   :: Value

-- Construct JSON dynamically
buildDynamic :: Value
buildDynamic = Object $ HM.fromList
  [ ("name",   String "Alice")
  , ("age",    Number 30)
  , ("active", Bool True)
  , ("scores", Array (V.fromList [Number 95, Number 87, Number 92]))
  , ("address", Null)
  ]

-- More idiomatic: use the object and array helpers
buildWithHelpers :: Value
buildWithHelpers = object
  [ "name"   .= ("Alice" :: Text)
  , "age"    .= (30 :: Int)
  , "active" .= True
  , "scores" .= ([95, 87, 92] :: [Int])
  , "address" .= Null
  ]

-- Pattern-match on Value to extract fields
extractName :: Value -> Maybe Text
extractName (Object obj) = case HM.lookup "name" obj of
  Just (String t) -> Just t
  _               -> Nothing
extractName _ = Nothing

-- Decode to Value first, then inspect
inspectUnknown :: BL.ByteString -> IO ()
inspectUnknown bs = case eitherDecode bs :: Either String Value of
  Left  err -> putStrLn ("Invalid JSON: " ++ err)
  Right val -> case val of
    Object obj -> do
      putStrLn "Got a JSON object with keys:"
      mapM_ (putStrLn . ("  " ++) . show) (HM.keys obj)
    Array arr  -> putStrLn ("Got a JSON array with " ++ show (V.length arr) ++ " elements")
    String t   -> putStrLn ("Got a string: " ++ show t)
    Number n   -> putStrLn ("Got a number: " ++ show n)
    Bool b     -> putStrLn ("Got a bool: " ++ show b)
    Null       -> putStrLn "Got null"

-- Merge two JSON objects
mergeObjects :: Value -> Value -> Value
mergeObjects (Object a) (Object b) = Object (HM.union b a)  -- b overrides a
mergeObjects _ b = b

-- Convert Value to a typed value
valueToUser :: Value -> Either String User
valueToUser = eitherDecode . encode  -- round-trip through JSON

The Value type is useful for webhook receivers, configuration parsers, and dynamic query builders where the JSON structure varies at runtime. For deeply nested access without pattern-matching boilerplate, lens-aeson (covered in the next section) provides concise traversal operators. The Scientific type used for Number values is an exact decimal representation — it avoids the floating-point precision loss you would get with Double, and you can convert it with toRealFloat or toBoundedInteger when you know the target type. See JSON best practices for guidance on when to prefer Value over typed parsing.

Lens-Aeson: Traversing JSON with Lenses

The lens-aeson package integrates Aeson with the lens library, providing concise operators for reading, traversing, and updating nested JSON values without verbose pattern-matching. Add lens-aeson and lens to your dependencies and import Data.Aeson.Lens.

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Aeson.Lens
import Control.Lens
import Data.Text (Text)

-- Sample nested JSON value
sampleJson :: Value
sampleJson = object
  [ "user" .= object
      [ "id"     .= (42 :: Int)
      , "name"   .= ("Alice" :: Text)
      , "email"  .= ("alice@example.com" :: Text)
      , "scores" .= ([95, 87, 92] :: [Int])
      , "address" .= object
          [ "city"    .= ("London" :: Text)
          , "country" .= ("UK" :: Text)
          ]
      ]
  , "version" .= (2 :: Int)
  ]

-- (^?) :: s -> Getting (First a) s a -> Maybe a
-- Safe traversal — returns Nothing if any key is missing

getName :: Maybe Text
getName = sampleJson ^? key "user" . key "name" . _String
-- Just "Alice"

getCity :: Maybe Text
getCity = sampleJson ^? key "user" . key "address" . key "city" . _String
-- Just "London"

getMissingField :: Maybe Text
getMissingField = sampleJson ^? key "user" . key "phone" . _String
-- Nothing (key "phone" does not exist)

-- _String, _Number, _Bool, _Null — typed prisms
getVersion :: Maybe Int
getVersion = sampleJson ^? key "version" . _Integer
-- Just 2  (using _Integer :: Prism' Value Integer)

-- _Array :: Prism' Value (Vector Value)
getScores :: Maybe [Value]
getScores = sampleJson ^? key "user" . key "scores" . _Array
  <&> toList
-- Just [Number 95.0, Number 87.0, Number 92.0]

-- values :: Traversal' Value Value — traverse array elements
allScores :: [Value]
allScores = sampleJson ^.. key "user" . key "scores" . values
-- [Number 95.0, Number 87.0, Number 92.0]

-- members :: Traversal' Value Value — traverse object values
userFieldValues :: [Value]
userFieldValues = sampleJson ^.. key "user" . members
-- [Number 42.0, String "Alice", String "alice@example.com", ...]

-- (.~) and (%~) — set or modify values
updateName :: Value -> Value
updateName v = v & key "user" . key "name" . _String .~ "Bob"

incrementVersion :: Value -> Value
incrementVersion v = v & key "version" . _Integer %~ (+1)

-- Construct Values with review (#)
constructString :: Value
constructString = review _String "hello"  -- String "hello"

constructNum :: Value
constructNum = review _Integer 42          -- Number 42.0

The (^?) operator is the workhorse for safe traversal — it returns Nothing if any part of the path is absent or has the wrong type, making it safe for untrusted input. Use (^..) with values or members to collect multiple values from arrays or objects. The nth combinator accesses array elements by index: json ^? key "items" . nth 0 . key "id" . _Integer. For TypeScript JSON type safety comparisons, lens-aeson's composable traversals are analogous to TypeScript's optional chaining (obj?.user?.name) but with compile-time type guarantees on the extracted value.

Servant: Type-Safe JSON API Endpoints

Servant is a Haskell library for defining web APIs at the type level. JSON encoding and decoding of request and response bodies is automatic — your types only need FromJSON and ToJSON instances. Servant uses the instances at compile time to generate type-safe handlers, client functions, and documentation.

{-# LANGUAGE DataKinds     #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}

import Servant
import Network.Wai.Handler.Warp (run)
import Data.Aeson               (FromJSON, ToJSON)
import GHC.Generics             (Generic)

-- ── Data types with JSON instances ───────────────────────────────
data User = User
  { userId   :: Int
  , userName :: String
  , userEmail :: String
  } deriving (Show, Generic, FromJSON, ToJSON)

data NewUser = NewUser
  { newUserName  :: String
  , newUserEmail :: String
  } deriving (Show, Generic, FromJSON, ToJSON)

-- ── API type definition (type-level DSL) ─────────────────────────
type UserAPI
  =    "users" :> Get  '[JSON] [User]
  :<|> "users" :> ReqBody '[JSON] NewUser :> Post '[JSON] User
  :<|> "users" :> Capture "id" Int :> Get '[JSON] User
  :<|> "users" :> Capture "id" Int :> DeleteNoContent

-- '[JSON] means "this endpoint uses JSON content type"
-- Servant calls toJSON on responses and parseJSON on request bodies
-- Servant returns 400 automatically if request body fails parseJSON

-- ── Handler implementations ───────────────────────────────────────
-- Handlers return Handler a (= ExceptT ServerError IO a)
getUsers :: Handler [User]
getUsers = return
  [ User 1 "Alice" "alice@example.com"
  , User 2 "Bob"   "bob@example.com"
  ]

createUser :: NewUser -> Handler User
createUser nu = do
  -- In real code: persist to database, get generated ID
  let newId = 3
  return User { userId = newId, userName = newUserName nu, userEmail = newUserEmail nu }

getUserById :: Int -> Handler User
getUserById uid = do
  let users = [User 1 "Alice" "alice@example.com", User 2 "Bob" "bob@example.com"]
  case filter ((== uid) . userId) users of
    (u:_) -> return u
    []    -> throwError err404 { errBody = "User not found" }

deleteUser :: Int -> Handler NoContent
deleteUser _uid = return NoContent  -- 204 No Content

-- ── Server: combine handlers with (:<|>) ─────────────────────────
server :: Server UserAPI
server = getUsers :<|> createUser :<|> getUserById :<|> deleteUser

-- ── Run with Warp ─────────────────────────────────────────────────
main :: IO ()
main = do
  putStrLn "Listening on port 8080"
  run 8080 (serve (Proxy :: Proxy UserAPI) server)

-- ── Type-safe client generation ───────────────────────────────────
-- Servant can generate client functions from the same API type:
-- import Servant.Client
-- getUsers'  :: ClientM [User]
-- createUser' :: NewUser -> ClientM User
-- (getUsers' :<|> createUser' :<|> _) = client (Proxy :: Proxy UserAPI)

Servant's type-level API definition is the key differentiator: if your handler returns a type that does not have a ToJSON instance, or if a ReqBody type lacks FromJSON, the code fails to compile — there are no runtime surprises. Servant also generates type-safe client functions (Servant.Client) from the same API type, and OpenAPI 3.0 documentation (servant-openapi3). For production Servant applications, add servant-auth for JWT authentication, servant-pagination for cursor-based pagination, and warp-tls for HTTPS termination.

Key Terms

Aeson
The standard Haskell JSON library, providing FromJSON and ToJSON typeclasses, a Value algebraic data type mirroring the JSON data model, and high-performance encoding and decoding functions. Aeson uses lazy ByteString as its wire format and an internal Builder for zero-copy encoding. Generic derivation via GHC.Generics lets you derive FromJSON and ToJSON instances with no boilerplate. Aeson decodes at approximately 300 MB/s on modern hardware, making it one of the fastest JSON libraries across all programming languages.
FromJSON typeclass
A Haskell typeclass defined in Data.Aeson with a single method parseJSON :: Value -> Parser a. Implement it to specify how an Aeson Value is converted to your Haskell type. decode and eitherDecode call parseJSON internally. Use withObject for JSON objects, withArray for arrays, and withText/withBool/withScientific for scalars — these helpers automatically emit a type-mismatch error if the Value constructor does not match. The (.:), (.:?), and (.!=) operators extract required, optional, and defaulted fields respectively.
ToJSON typeclass
A Haskell typeclass in Data.Aeson with methods toJSON :: a -> Value and toEncoding :: a -> Encoding. toJSON converts a Haskell value to an intermediate Value tree; toEncoding writes directly to Aeson's Builder without constructing a Value, which is significantly faster for serialization-heavy workloads. Always implement both methods when writing manual instances. Use the object helper and (.=) operator to construct Values concisely: object ["name" .= userName u, "id" .= userId u].
eitherDecode
An Aeson function with type FromJSON a => ByteString -> Either String a that parses a lazy ByteString into a typed Haskell value. On success it returns Right a; on failure it returns Left String where the string is a human-readable error message including a JSONPath-style location (e.g., Error in $.user.age: parsing Int failed). Prefer eitherDecode over decode in all production code. For strict ByteString, use eitherDecodeStrict. For Text or String input, use eitherDecodeStrictText (Aeson 2.x).
Value type
Data.Aeson.Value is a Haskell algebraic data type with six constructors that exactly mirror the JSON specification: Object (HashMap Text Value), Array (Vector Value), String Text, Number Scientific, Bool Bool, and Null. It is both the intermediate representation used internally by Aeson (all FromJSON/ToJSON instances operate on Value) and a concrete type you can parse into when the JSON schema is dynamic or unknown. Scientific for numbers provides exact decimal representation, avoiding the floating-point imprecision of Double.
Servant
A Haskell web framework that defines HTTP APIs at the type level using a domain-specific language of type operators (:>, :<|>). JSON request body parsing and response encoding are specified in the API type as ReqBody '[JSON] MyType and Get '[JSON] MyType — Servant automatically calls parseJSON and toJSON using the type's FromJSON/ToJSON instances, returning a 400 error if body parsing fails. The same API type can generate type-safe client functions (Servant.Client), mock servers (Servant.Mock), and OpenAPI documentation (servant-openapi3).

FAQ

How do I parse JSON in Haskell?

Use the Aeson library. Add aeson to your .cabal or stack.yaml dependencies, then import Data.Aeson. For a type that derives Generic, add {-# LANGUAGE DeriveGeneric #-} and deriving (Generic, FromJSON, ToJSON). Then decode :: FromJSON a => ByteString -> Maybe a parses a lazy ByteString into Maybe a (Nothing on failure). For production, prefer eitherDecode :: FromJSON a => ByteString -> Either String a, which returns a descriptive error message on Left. If your input is a strict ByteString, use eitherDecodeStrict directly without conversion.

What is the difference between decode and eitherDecode in Aeson?

decode :: FromJSON a => ByteString -> Maybe a returns Nothing on any parse failure and discards the error reason — you know parsing failed but not why. eitherDecode :: FromJSON a => ByteString -> Either String a returns Left "error message" on failure with a JSONPath-style location such as Error in $.age: parsing Int failed, expected Number, but encountered String. In production, always use eitherDecode so you can log the error or return a meaningful 400 response to the client. The Strict variants (decodeStrict, eitherDecodeStrict) accept strict ByteString without needing Data.ByteString.Lazy.fromStrict.

How do I automatically derive JSON instances in Haskell?

Enable the DeriveGeneric language extension with {-# LANGUAGE DeriveGeneric #-} at the top of the file, import GHC.Generics (Generic) and Data.Aeson, then add deriving (Generic, FromJSON, ToJSON) to your data type declaration. Aeson uses the Generic typeclass to generate FromJSON and ToJSON instances at compile time with zero boilerplate. By default, record field names become JSON keys as-is. To customize key names — for example, dropping a leading underscore or converting camelCase to snake_case — use genericParseJSON and genericToJSON with a custom Options value.

How do I handle JSON parsing errors in Haskell?

Use eitherDecode instead of decode — it returns Either String a, giving you a human-readable error message including a JSONPath location on Left. In a web application, pattern-match on the result: case eitherDecode body of { Left err -> return (badRequest err); Right val -> process val }. For custom parseJSON implementations, use the Parser monad's fail function to emit descriptive errors — fail ("expected positive Int, got: " ++ show n). Aeson prepends the field path automatically, producing messages like Error in $.items[0].price: expected positive Int, got: -5.

How do I rename fields in Haskell JSON serialization?

Use genericParseJSON and genericToJSON with a custom Options value instead of the default derived instances. The fieldLabelModifier field in Options is a String -> String function applied to each record field name before it becomes a JSON key. Common patterns: drop 1 strips the leading underscore from _fieldName, camelTo2 '_' from Data.Aeson.Types converts camelCase to snake_case. Define both instance FromJSON and instance ToJSON with the same options to keep encoding and decoding symmetric.

What is the Value type in Aeson?

Data.Aeson.Value is a Haskell sum type that mirrors the JSON data model exactly: Object (a HashMap Text Value), Array (a Vector Value), String Text, Number Scientific, Bool Bool, and Null. It lets you work with JSON whose structure is unknown at compile time — parse a ByteString into a Value with decode or eitherDecode without specifying a target type, then pattern-match or use lens-aeson operators to extract nested values. Value is also the intermediate representation Aeson uses internally: toJSON converts a Haskell value to Value and parseJSON converts a Value back to a Haskell type.

How do I traverse JSON in Haskell with lenses?

Add the lens-aeson package and import Data.Aeson.Lens. The key operators are (^?) for safe traversal returning Maybe, (^..) for collecting multiple values into a list, and (.~)/(%~) for setting or modifying values. Use key "fieldName" to descend into an object field, _Array to work with arrays, and _String, _Integer, _Bool, _Null to extract typed values. Example: value ^? key "user" . key "name" . _String returns Maybe Text. Chain multiple key lenses for nested access. The values traversal iterates all array elements.

How do I build a JSON API in Haskell with Servant?

Define your API type using Servant's type-level DSL with the DataKinds and TypeOperators extensions. For JSON endpoints, use '[JSON] as the content-type list: type MyAPI = "users" :> Get '[JSON] [User] :<|> "users" :> ReqBody '[JSON] NewUser :> Post '[JSON] User. Servant automatically calls toJSON on response values and parseJSON on request bodies — your types just need ToJSON and FromJSON instances. Implement handlers as Handler values: getUsers :: Handler [User]. Combine with (:<|>) and run with Warp: run 8080 (serve (Proxy :: Proxy MyAPI) server).

Further reading and primary sources