Skip to content

wireform-toml

wireform-toml reads and writes TOML configuration for Haskell services and tools. TOML’s table sections, inline tables, and array-of-tables map naturally onto Haskell records when you derive Generic, while the encoder places [section] headers and [parent.child] paths correctly on output. The parser is validated against the upstream toml-test suite for both TOML 1.0 and 1.1.

CapabilityWhy it matters
deriveTOML Template Haskell deriverLoad config files into typed records with wireform-derive annotations; Generic defaults work for simple cases
Section-aware pretty printing[database] and nested [database.pool] headers land in sensible order
Datetime supportRFC 3339 offsets and local datetimes as first-class values
Inline and standard tablesCompact inline { key = "val" } or full [table] blocks
Array-of-tables[[items]] repeated sections for list-of-struct configs
toml-test conformanceConfidence that edge cases match the spec

Derive codecs with the Template Haskell deriver and round-trip with encodeTOML / decodeTOML.

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
import GHC.Generics (Generic)
import Data.Text (Text)
import TOML.Class (ToTOML, FromTOML, encodeTOML, decodeTOML)
import TOML.Derive (deriveTOML)
data Database = Database
{ dbHost :: !Text
, dbPort :: !Int
} deriving stock (Generic)
data AppConfig = AppConfig
{ appName :: !Text
, database :: !Database
} deriving stock (Generic)
$(deriveTOML ''Database)
$(deriveTOML ''AppConfig)
loadConfig :: Text -> Either String AppConfig
loadConfig = decodeTOML

For simple cases with no wire-format customization, Generic defaults also work: add deriving Generic and declare empty instance ToTOML Database and instance FromTOML Database declarations (and likewise for AppConfig).

Nested records become TOML subtables. The encoder emits a [database] section with keys under it rather than flattening everything at the top level.

TOML datetimes live in TOML.Value as TLocalDateTime, TOffsetDateTime, and related constructors. Deriving handles them when fields use the corresponding Haskell types from the value module.

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
import GHC.Generics (Generic)
import Data.Text (Text)
import TOML.Class (FromTOML, decodeTOML)
import TOML.Derive (deriveTOML)
data Job = Job
{ jobName :: !Text
, scheduledAt :: !Text
} deriving stock (Generic)
$(deriveTOML ''Job)
parseJob :: Text -> Either String Job
parseJob = decodeTOML

TOML datetimes decode as Text in the value layer (TDateTime, TDate, TTime). Map them into time types in application code when you need calendar arithmetic.

encodeTOMLDirect routes through toEncoding when you want the same path the TH deriver uses, which can avoid an extra conversion for complex nested values.

import TOML.Class (ToTOML, encodeTOMLDirect)
writeConfig :: ToTOML a => a -> Text
writeConfig = encodeTOMLDirect

Use TOML.Decode.decode on raw text when you need the untyped TOML.Value AST before mapping into application types.

wireform-toml encode + decode (Person record) wireform-toml encode + decode (Person record) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5. Decode is now linear in input size (was previously O(N²) due to T.index/T.length on the full source); 100-record decode dropped from 240 ms to 335 µs. 0 125000 250000 375000 500000 730 2383 86012 343876 single Person [Person] x 100 encode decode wireform-toml encode + decode (Person record) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5. Decode is now linear in input size (was previously O(N²) due to T.index/T.length on the full source); 100-record decode dropped from 240 ms to 335 µs. 0 125000 250000 375000 500000 730 2383 86012 343876 single Person [Person] x 100 encode decode
Operationencodedecoderatio
single Person730 ns2383 ns3.26x
[Person] x 10086012 ns343876 ns3.100x

Last run 2026-06-27 11:35:55 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5. Decode is now linear in input size (was previously O(N²) due to T.index/T.length on the full source); 100-record decode dropped from 240 ms to 335 µs..

Decode scales linearly with input size.

The chart and table above are regenerated by wireform-stats from wireform-toml/bench-results/summary/toml-encode-decode.json — the same source the README chart is built from.

ModuleRole
TOML.ClassToTOML / FromTOML, encodeTOML, decodeTOML
TOML.ValueAST for tables, arrays, datetimes, and inline tables
TOML.EncodeSection-aware TOML writer
TOML.DecodeParser for TOML 1.0 / 1.1 documents
TOML.EncodingIntermediate encoding type used by the deriver
TOML.DeriveTemplate Haskell deriver with Wireform.Derive annotations