Code Generators
JSON to Haskell Converter
Paste a JSON or XML sample — the format is auto-detected — and get typed Haskell models with names, nesting and nullability inferred for you.
In short
Convert JSON to Haskell online: paste a sample and generate typed Haskell models with serialization helpers — powered by quicktype, in your browser.
By NaveenKumar T · Updated
- JSON or XML input, auto-detected
- Nested objects become types
- Nullability inferred
Runs entirely in your browser — nothing you paste or open here is uploaded, logged, or stored.How we handle data →
Example
Example: JSON order payload to Haskell
A small order document with a nested object, an array of two items, a decimal total and a null field — enough to show how each Haskell type, list and nullable is inferred. The same page accepts the XML form of this order too, so the sample below is an example rather than the only accepted format. The right-hand side is the exact output of this tool.
{
"id": "ord_9F2KX",
"status": "processing",
"paid": true,
"customer": { "name": "Ada Lovelace", "email": "ada@example.com", "phone": null },
"items": [
{ "sku": "KB-75", "qty": 1, "price": 149.99 },
{ "sku": "MS-12", "qty": 2, "price": 39.5 }
],
"total": 228.99
}{-# LANGUAGE StrictData #-}
{-# LANGUAGE OverloadedStrings #-}
module QuickType
( Root (..)
, Customer (..)
, Item (..)
, decodeTopLevel
) where
import Data.Aeson
import Data.Aeson.Types (emptyObject)
import Data.ByteString.Lazy (ByteString)
import Data.HashMap.Strict (HashMap)
import Data.Text (Text)
import Data.Vector (Vector)
data Root = Root
{ rootIDRoot :: Text
, statusRoot :: Text
, paidRoot :: Bool
, customerRoot :: Customer
, itemsRoot :: Vector Item
, totalRoot :: Float
} deriving (Show)
data Customer = Customer
{ nameCustomer :: Text
, emailCustomer :: Text
, phoneCustomer :: Maybe Text
} deriving (Show)
data Item = Item
{ skuItem :: Text
, qtyItem :: Int
, priceItem :: Float
} deriving (Show)
decodeTopLevel :: ByteString -> Maybe Root
decodeTopLevel = decode
instance ToJSON Root where
toJSON (Root rootIDRoot statusRoot paidRoot customerRoot itemsRoot totalRoot) =
object
[ "id" .= rootIDRoot
, "status" .= statusRoot
, "paid" .= paidRoot
, "customer" .= customerRoot
, "items" .= itemsRoot
, "total" .= totalRoot
]
instance FromJSON Root where
parseJSON (Object v) = Root
<$> v .: "id"
<*> v .: "status"
<*> v .: "paid"
<*> v .: "customer"
<*> v .: "items"
<*> v .: "total"
instance ToJSON Customer where
toJSON (Customer nameCustomer emailCustomer phoneCustomer) =
object
[ "name" .= nameCustomer
, "email" .= emailCustomer
, "phone" .= phoneCustomer
]
instance FromJSON Customer where
parseJSON (Object v) = Customer
<$> v .: "name"
<*> v .: "email"
<*> v .: "phone"
instance ToJSON Item where
toJSON (Item skuItem qtyItem priceItem) =
object
[ "sku" .= skuItem
, "qty" .= qtyItem
, "price" .= priceItem
]
instance FromJSON Item where
parseJSON (Object v) = Item
<$> v .: "sku"
<*> v .: "qty"
<*> v .: "price"
Look for the "phone" field: sampled as null, it is typed as Maybe Text. "items" became Vector Item, and "total" is Float.
Learn more
From JSON sample to Haskell types
Hand-writing model classes from a JSON payload is transcription work: read a field, type a property, repeat eighty times, miss one. This page runs quicktype (the same engine behind the best-known generators) against your document, merging array elements to find the common shape and naming nested types from their keys.
Every record field is suffixed with its type name (nameCustomer, totalRoot) to avoid Haskell's shared selector namespace. If that reads badly in your code, enable DuplicateRecordFields and OverloadedRecordDot in the module and rename the fields — the FromJSON/ToJSON instances reference them by name, so rename both places consistently.
How JSON values become Haskell types
Inference works from the values in your sample. Strings map to Text, integers and decimals are told apart (228.99 becomes Float, while 1 and 2 stay integral), true/false become the language's boolean, and an object such as "customer" becomes its own named type. The "items" array is typed as Vector Item because every element shares the same keys — quicktype unifies the shapes of all array elements into one Item type.
Nulls are the one place inference needs your help: "phone": null carries no type information, so the field is emitted as Maybe Text. If the real API sometimes sends a string there, include a sample where the field is populated (paste several documents merged into one array) and the generated type becomes the union of what it saw.
Pasting XML instead: how elements and attributes become Haskell types
The XML is parsed into a data model before inference (attributes included, text nodes named "text"), so the root element becomes a nested type: <order> is a field of type Order on the top-level Root rather than the top level itself. Child elements become fields, and repeated elements — the two <item> siblings — are collected into Vector Item under an Items wrapper type that mirrors the <items> container.
Attributes and element text are typed differently, and this matters: element text is type-inferred, so <total>228.99</total> becomes Float, while attribute values are kept as they were written — qty="1" and price="149.99" stay Text in Haskell. Prefer element text for numeric data you want typed, or cast attribute values after decoding. One more trap: a single <item> would not be recognized as a list, so make sure repeated elements appear at least twice in the sample.
Annotations, nulls and nested arrays in the generated Haskell
Instead of annotations quicktype writes the Aeson instances out in full: ToJSON builds an object list pairing each wire key with its record field, and FromJSON reads them back with the .: operator, switching to .:? for keys that were missing from some samples. Record fields are suffixed with their type name — nameCustomer, totalRoot — because Haskell record selectors share one namespace per module. The module also enables StrictData and OverloadedStrings, derives Show, and exports decodeTopLevel, a ByteString to Maybe Root wrapper around decode.
A field whose only sampled value is null comes back as Maybe Text, which is the right shape more by luck than by inference — Aeson turns a null into Nothing. Add a document where the field carries a value and the same Maybe now means what you think it means. Keys absent from some samples are parsed with .:? instead, so absence and null both land in Nothing, and encoding writes null back. An empty array is typed as a Vector of Maybe Text: a guess, not a contract.
Nested objects become their own record types named from the key — Customer, Item — each with its own ToJSON and FromJSON instances, and arrays of objects are typed Vector Item, which is why the module needs vector alongside aeson. One thing to fix by hand before shipping: the generated parseJSON equations only match the Object case, with no fallback, so a non-object where a record was expected fails with a pattern-match error rather than a clean parse failure. Add a catch-all clause.
Help
Frequently asked questions
Everything you need to know about the JSON to Haskell.
01How do I generate Haskell classes from JSON?
Paste a representative document — JSON or XML, the format is detected automatically — and press Convert. A real API response works best. Nested objects become their own Haskell types, arrays become Vector Item, and the result downloads as Root.hs.
02What does the generated Haskell code include?
Haskell output defines record types with Aeson FromJSON/ToJSON instances — field mappings handled, Maybe wrapping nullables — your payload as algebraic data types.
03How do I use the generated Haskell code in my project?
Add aeson (and vector) to your cabal or stack dependencies and add the module. decodeTopLevel :: ByteString -> Maybe Root parses, and Data.Aeson's encode serializes through the generated ToJSON instances. Record fields are suffixed with their type name (nameCustomer, totalRoot) to avoid clashes, and the FromJSON instances map them back to the wire names.
04How do the generated Haskell instances handle field names?
Via explicit Aeson parsers with the original JSON keys, so wire names survive even where they'd clash with Haskell record conventions. Nullable fields arrive as Maybe a, exactly as you'd model them by hand.
Keep working