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" .!= FalseThe (.:) 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 JSONThe 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.0The (^?) 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
FromJSONandToJSONtypeclasses, aValuealgebraic data type mirroring the JSON data model, and high-performance encoding and decoding functions. Aeson uses lazyByteStringas its wire format and an internalBuilderfor zero-copy encoding. Generic derivation via GHC.Generics lets you deriveFromJSONandToJSONinstances 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.Aesonwith a single methodparseJSON :: Value -> Parser a. Implement it to specify how an AesonValueis converted to your Haskell type.decodeandeitherDecodecallparseJSONinternally. UsewithObjectfor JSON objects,withArrayfor arrays, andwithText/withBool/withScientificfor scalars — these helpers automatically emit a type-mismatch error if theValueconstructor does not match. The(.:),(.:?), and(.!=)operators extract required, optional, and defaulted fields respectively. - ToJSON typeclass
- A Haskell typeclass in
Data.Aesonwith methodstoJSON :: a -> ValueandtoEncoding :: a -> Encoding.toJSONconverts a Haskell value to an intermediateValuetree;toEncodingwrites directly to Aeson'sBuilderwithout constructing aValue, which is significantly faster for serialization-heavy workloads. Always implement both methods when writing manual instances. Use theobjecthelper and(.=)operator to constructValues concisely:object ["name" .= userName u, "id" .= userId u]. - eitherDecode
- An Aeson function with type
FromJSON a => ByteString -> Either String athat parses a lazyByteStringinto a typed Haskell value. On success it returnsRight a; on failure it returnsLeft Stringwhere the string is a human-readable error message including a JSONPath-style location (e.g.,Error in $.user.age: parsing Int failed). PrefereitherDecodeoverdecodein all production code. For strictByteString, useeitherDecodeStrict. ForTextorStringinput, useeitherDecodeStrictText(Aeson 2.x). - Value type
Data.Aeson.Valueis 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, andNull. It is both the intermediate representation used internally by Aeson (allFromJSON/ToJSONinstances operate onValue) and a concrete type you can parse into when the JSON schema is dynamic or unknown.Scientificfor numbers provides exact decimal representation, avoiding the floating-point imprecision ofDouble.- 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 asReqBody '[JSON] MyTypeandGet '[JSON] MyType— Servant automatically callsparseJSONandtoJSONusing the type'sFromJSON/ToJSONinstances, 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
- Aeson Hackage Documentation — Official Haskell Aeson library reference with FromJSON, ToJSON, and Options documentation
- Aeson Tutorial (School of Haskell) — Comprehensive introduction to Aeson with worked examples for parsing and encoding
- lens-aeson Hackage — Lens-based JSON traversal operators for Aeson Value types
- Servant Tutorial — Official Servant documentation covering API type definition, handlers, and JSON integration
- Real World Haskell: JSON — Real World Haskell coverage of JSON processing and Aeson patterns in production code