Skip to content

wireform-yaml

wireform-yaml implements YAML 1.2 read and write paths for Haskell applications. Configuration files, CI manifests, and Kubernetes-style multi-doc streams all map cleanly onto typed records via Generic, while the value layer still exposes anchors, aliases, tags, and both block and flow styles when you need full YAML expressiveness. The decoder and emitter are validated against the upstream yaml-test-suite with 100% conformance.

CapabilityWhy it matters
deriveYAML Template Haskell deriverDerive config types with wireform-derive annotations; Generic defaults work for simple cases
Block and flow stylesHuman-readable block output; compact flow for inline structures
Anchors and aliasesPreserve shared references and cyclic graphs in the value layer
TagsExplicit scalar typing (!!int, application-specific tags)
Literal and folded scalarsRound-trip multiline strings (| and >)
Multi-document streams--- separated documents for kubectl-style files
YAML 1.2 core schemaPlain scalars that look like bools or numbers stay quoted on encode

Derive codecs with the Template Haskell deriver and use encodeYAML / decodeYAML for the common case.

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
import GHC.Generics (Generic)
import Data.Text (Text)
import YAML.Class (ToYAML, FromYAML, encodeYAML, decodeYAML)
import YAML.Derive (deriveYAML)
data Server = Server
{ host :: !Text
, port :: !Int
, tls :: !Bool
} deriving stock (Generic)
$(deriveYAML ''Server)
loadConfig :: Text -> Either String Server
loadConfig = decodeYAML

For simple cases with no wire-format customization, Generic defaults also work: add deriving Generic and declare empty instance ToYAML Server and instance FromYAML Server declarations.

Field naming follows the same Wireform.Derive annotation vocabulary as other wireform formats (rename, omitEmpty, and friends via YAML.Derive).

When the shape is dynamic or you must preserve YAML-specific features, work in YAML.Value and encode with YAML.Encode.

{-# LANGUAGE OverloadedStrings #-}
import Data.Text (Text)
import qualified YAML.Decode as YD
import qualified YAML.Encode as YE
roundtripAnchors :: Text -> Either String Text
roundtripAnchors doc = do
val <- YD.decode doc
pure (YE.encode val)

Kubernetes and other tools emit several YAML documents in one file. Decode the stream as a YAML.Value.Stream, or encode many values with encodeStream.

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Vector as V
import YAML.Encode (encodeStream)
import YAML.Value (Document(..), Stream(..), mapping, string)
writeStream :: Stream -> Text
writeStream = encodeStream
exampleStream :: Stream
exampleStream =
Stream $
V.fromList
[ Document True False (mapping [(string "apiVersion", string "v1")])
, Document True False (mapping [(string "kind", string "ConfigMap")])
]

The emitter chooses block style by default and falls back to flow for empty containers. Output is round-trippable: encode followed by decode recovers the same Value.

wireform-yaml is a pure Haskell parser that consistently outperforms the C libyaml bindings by 3-32x across all input sizes, making it one of the fastest YAML parsers in any language. Against HsYAML (the other pure Haskell option), it is 244-408x faster.

wireform-yaml vs libyaml (C bindings via Hackage yaml package)

Section titled “wireform-yaml vs libyaml (C bindings via Hackage yaml package)”
wireform-yaml decode vs libyaml across input sizes wireform-yaml decode vs libyaml across input sizes lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 50.0 100 150 200 0.440 6.14 6.73 34.6 7.51 44.2 4.71 11.2 37.5 122 tiny small flow literal big wireform-yaml yaml (libyaml) wireform-yaml decode vs libyaml across input sizes lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 50.0 100 150 200 0.440 6.14 6.73 34.6 7.51 44.2 4.71 11.2 37.5 122 tiny small flow literal big wireform-yaml yaml (libyaml)
Operationwireform-yamlyaml (libyaml)ratio
tiny0.44 µs6.14 µs13.95x
small6.73 µs34.6 µs5.15x
flow7.51 µs44.2 µs5.88x
literal4.71 µs11.2 µs2.38x
big37.5 µs122 µs3.26x

Last run 2026-11-02 14:35:47 UTC. ghc-9.10.3 on darwin-aarch64, criterion 1.6.5.

wireform-yaml decode vs HsYAML (pure Haskell) across input sizes wireform-yaml decode vs HsYAML (pure Haskell) across input sizes lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 2500 5000 7500 10000 0.440 119 6.73 1068 7.51 1322 4.71 1504 37.5 5027 tiny small flow literal big wireform-yaml HsYAML wireform-yaml decode vs HsYAML (pure Haskell) across input sizes lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 2500 5000 7500 10000 0.440 119 6.73 1068 7.51 1322 4.71 1504 37.5 5027 tiny small flow literal big wireform-yaml HsYAML
Operationwireform-yamlHsYAMLratio
tiny0.44 µs119 µs270.86x
small6.73 µs1068 µs158.75x
flow7.51 µs1322 µs176.03x
literal4.71 µs1504 µs319.30x
big37.5 µs5027 µs133.95x

Last run 2026-11-02 14:35:47 UTC. ghc-9.10.3 on darwin-aarch64, criterion 1.6.5.

Escaped strings and derived records vs libyaml Escaped strings and derived records vs libyaml lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 5.00 10.0 15.0 20.0 7.12 13.6 1.85 11.9 1.25 5.36 escaped decode record decode record encode wireform-yaml yaml (libyaml) Escaped strings and derived records vs libyaml lower is better · µs · ghc-9.10.3 on darwin-aarch64, criterion 1.6.5 0 5.00 10.0 15.0 20.0 7.12 13.6 1.85 11.9 1.25 5.36 escaped decode record decode record encode wireform-yaml yaml (libyaml)
Operationwireform-yamlyaml (libyaml)ratio
escaped decode7.12 µs13.6 µs1.91x
record decode1.85 µs11.9 µs6.43x
record encode1.25 µs5.36 µs4.29x

Last run 2026-11-02 14:35:47 UTC. ghc-9.10.3 on darwin-aarch64, criterion 1.6.5.

The record fixture uses the actual deriveYAML output. Every benchmark fixture is checked for successful decoding before timing; the HsYAML comparison forces the parsed contents as well as the outer document list.

ASCII validation scans eight bytes at a time and skips line normalization when there are no carriage returns. Unicode and unusual controls retain lexical validation. Literal scalars measure their final chomped length and copy their UTF-8 lines into one exact buffer, so discarded trailing blank lines are neither copied nor retained. Escaped scalars likewise write directly into a bounded UTF-8 buffer. Optimized Core shows bulk byte copies, direct byte writes, and unboxed offset loops without per-escape result allocation.

The charts and tables above are regenerated by wireform-stats from wireform-yaml/bench-results/summary/*.json — the same source the README charts are built from.

ModuleRole
YAML.ClassToYAML / FromYAML, encodeYAML, decodeYAML
YAML.ValueAST for mappings, sequences, scalars, anchors, and streams
YAML.EncodeBlock and flow emitter with YAML 1.2 quoting rules
YAML.DecodeParser for documents and multi-doc streams
YAML.DeriveTemplate Haskell deriver wired to Wireform.Derive annotations
YAML.JSONBridge between YAML values and JSON