Skip to content

Formatters & Code

JSON to Haskell Data Type

Haskell records with FromJSON / ToJSON instances.

Runs in your browser
JSON · source
lines: 17chars: 261size: 261 B
Haskell data type · result
lines: 30chars: 574size: 574 B
live

Understanding JSON → Haskell

Records, deriving, and FromJSON.

Why Haskell JSON is unusually pleasant once you embrace deriving, and the GHC.Generics dance that produces a working decoder from a record declaration.

aeson is the library.

aeson is the de-facto Haskell JSON library — fast, well-maintained, driven by two typeclasses: FromJSON for decoding and ToJSON for encoding. A type that has instances of both can round-trip through JSON. Writing those instances by hand is tedious; the generic deriving path generates them from the record's field names automatically.

Records and generic deriving.

A Haskell record: data User = User { userId :: Int, name :: Text, avatar :: Maybe Text } deriving (Show, Generic). The Generic derived instance is what aeson hooks into. Then instance FromJSON User with no method body — the compiler synthesizes the decoder via Generic. Same for instance ToJSON User. Three lines for a fully round-trippable record.

Field naming via Options.

Haskell record fields are camelCase; JSON keys are typically snake_case. Bridge them by passing a non-default Options record: instance FromJSON User where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelTo2 '_' }. camelTo2 '_' turns userId into user_id for JSON purposes. The same options govern other transformations (strip prefixes, omit Nothing, tag-based encoding for sum types).

A worked example.

From { "user_id": 7, "name": "Q", "avatar": null }: {-# LANGUAGE DeriveGeneric #-} import Data.Aeson import GHC.Generics data User = User { userId :: Int , name :: String , avatar :: Maybe String } deriving (Show, Generic) instance FromJSON User where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelTo2 '_' } instance ToJSON User where toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelTo2 '_' } That's the whole DTO. The encoder uses toEncoding rather than toJSON because it streams directly to a Builder — measurably faster on large outputs.

Maybe for nullable

avatar :: Maybe String

Nothing on absent or null; Just on a present value.

JSON null or missing key → Nothing

= Type-safe optional

Sum types and discriminated unions.

A Haskell sum type is a discriminated union by language design. data Event = Click { x :: Int, y :: Int } | Submit { form :: String } plus the right Options encodes each constructor as a JSON object with a "tag" field (or a custom discriminator). Decoding pattern-matches on the tag and dispatches to the right constructor. This is one of the places where Haskell's type system makes something that's awkward in dynamically-typed languages feel natural.

Strict vs lazy fields.

Haskell records have lazy fields by default — accessing a field forces evaluation, which is fine until the JSON has been kept around for ages and you're forcing fields a long time after parse. For DTOs that should be fully evaluated at parse time, add strictness annotations with !: data User = User { userId :: !Int, name :: !Text, ... }. The aeson parser respects these and forces the fields during decode. Costs nothing measurable for typical DTOs; saves you from space-leak bugs that are unusually hard to debug.

Text vs String.

The default Haskell String is a linked list of Char — slow, memory-inefficient, almost never what you actually want. Almost every modern Haskell project uses Data.Text.Text instead — packed UTF-16, comparable to other languages' string type. Set {-# LANGUAGE OverloadedStrings #-} at the top of the file and string literals become Text automatically. Codegen tools targeting modern Haskell should emit Text, not String.

Frequently asked questions

Quick answers.

Which Haskell libraries are supported?

The generator focuses on the `Aeson` library, producing `FromJSON` and `ToJSON` instances compatible with modern Haskell development.

How does it handle nested objects?

Nested JSON objects are converted into separate Haskell data types, with the parent record referencing these types as fields.

Are Haskell naming conventions followed?

Yes. The tool converts snake_case or camelCase JSON keys into valid Haskell record fields, typically prepending the type name to avoid namespace collisions.

Can I use this for complex arrays?

Arrays of objects are mapped to Haskell lists of a specific type, while mixed-type arrays may require manual adjustment into a Sum type or `Value`.

People also search for

Related tools

More in this room.

See all in Formatters & Code