Getting started
Working code before theory. The wireform repo is the fastest playground; your own Cabal package is only a few lines more.
Prerequisites
Section titled “Prerequisites”| You need | Notes |
|---|---|
| GHC | 9.6+ recommended; GHC2021. |
| cabal-install | 3.x |
| Optional: Nix | nix develop gives pinned GHC + pre-built deps. |
No Docker, no protoc, and no language-specific runtimes needed.
Step 1: Run something
Section titled “Step 1: Run something”cd wireform-cabal updatecabal run example-msgpackOutput: a Person record round-tripped through MessagePack via Generic
defaults. No schema, no codegen. This is the lowest-ceremony path.
Try a few more to see the range:
cabal run example-protobuf # protobuf message instancescabal run example-xml # Generic XML encode/decodecabal run example-avro # Avro schema + value APIcabal run example-cbor # CBOR encode/decodecabal run example-parquet # Parquet footer metadataStep 2: Pick a workflow
Section titled “Step 2: Pick a workflow”For MessagePack, CBOR, BSON, YAML, TOML, EDN, Ion, Bencode, CSV, XML, HTML.
- Define records with
Generic. - Derive the format’s classes.
- Call the typed encode/decode.
import MsgPack.Class (ToMsgPack, FromMsgPack, encodeMsgPack, decodeMsgPack)
data Person = Person { name :: !Text, age :: !Int } deriving stock (Show, Eq, Generic) deriving anyclass (ToMsgPack, FromMsgPack)
let bytes = encodeMsgPack (Person "Ada" 36)Same pattern for ToCBOR/FromCBOR, ToBSON/FromBSON, ToXML/FromXML,
ToYAML/FromYAML, ToTOML/FromTOML, etc. Derive multiple at once:
deriving anyclass (ToMsgPack, FromMsgPack, ToCBOR, FromCBOR, ToYAML, FromYAML)Best when: you control both ends of the wire and want little ceremony.
Three codegen options:
| Approach | When to use |
|---|---|
$(loadProto "file.proto") | Small projects, TH is fine, want types in the same module |
wireform-gen proto -i file.proto -o gen/ | Larger projects, CI-friendly, committed generated code |
protoc --wireform_out=DIR | Your org standardizes on protoc |
Then:
import qualified Proto.Encode as Plet bytes = P.encodeMessage myMessageBest when: you interoperate with other languages, use gRPC, or need field evolution.
For Avro, CDDL, Ion ISL, XSD, etc.
- Load or build a schema.
- Encode/decode
Valuetrees against it. - Or generate typed Haskell with
wireform-gen.
Best when: data pipelines, registries, Avro container files, analytics.
For Parquet, Arrow, Iceberg, ORC. These formats have full or partial reader/writer support:
import Parquet.Read (readParquet)import Iceberg.JSON (metadataFromJSON)import ORC.Read (readColumn)import Arrow.IPC (decodeRecordBatch)Best when: you need to work with columnar data files without pulling in heavyweight JVM tooling.
wireform includes a full pipeline, not just encode/decode:
| Step | Module |
|---|---|
| SAX event stream | XML.SAX |
| Zero-copy DOM | XML.FastDOM |
| Tree DOM | XML.Decode |
| XPath queries | XML.Path |
| CSS selectors (HTML) | HTML.Selector |
| XSLT transforms | XML.XSLT |
| Concurrent chunk parsing | XML.Incremental |
| Streaming HTML rewriter | HTML.Rewriter |
Best when: scraping, config parsing, XML pipeline tooling.
Step 3: Use wireform from your own package
Section titled “Step 3: Use wireform from your own package”Path dependency (typical while hacking or before Hackage)
Section titled “Path dependency (typical while hacking or before Hackage)”~/Code/ wireform-/ # this repo my-app/ cabal.project my-app.cabal app/Main.hscabal.project:
packages: . ../wireform-my-app.cabal:
cabal-version: 3.0name: my-appversion: 0.1.0.0build-type: Simple
executable my-app main-is: Main.hs hs-source-dirs: app default-language: GHC2021 build-depends: base ^>=4.18 , text ^>=2.0 , bytestring ^>=0.11 , wireform default-extensions: OverloadedStrings DeriveGeneric DerivingStrategies DeriveAnyClassapp/Main.hs:
module Main where
import Data.Text (Text)import qualified Data.ByteString as BSimport GHC.Generics (Generic)import MsgPack.Class (ToMsgPack, FromMsgPack, encodeMsgPack, decodeMsgPack)
data Person = Person { name :: !Text , age :: !Int } deriving stock (Show, Eq, Generic) deriving anyclass (ToMsgPack, FromMsgPack)
main :: IO ()main = do let bytes = encodeMsgPack (Person "Ada" 36) putStrLn $ "Encoded to " <> show (BS.length bytes) <> " bytes" case decodeMsgPack bytes of Right p -> print (p :: Person) Left e -> putStrLn $ "decode failed: " <> ecd ~/Code/my-app && cabal run my-appStep 4: Template Haskell for protobufs
Section titled “Step 4: Template Haskell for protobufs”Put .proto files under proto/ and splice them in:
{-# LANGUAGE TemplateHaskell #-}import Proto.TH (loadProto)$(loadProto "proto/person.proto")For imports across files, pass include dirs:
import Proto.TH (loadProtoWith, defaultLoadOpts, LoadOpts(..))$(loadProtoWith defaultLoadOpts { loIncludeDirs = ["proto", "."] } "proto/api.proto")Reference: cabal run example-th in the wireform repo.
Step 5: Code generation with wireform-gen
Section titled “Step 5: Code generation with wireform-gen”No global install needed:
cabal exec wireform-gen -- proto -i proto/person.proto -o gen/cabal exec wireform-gen -- avro -i schemas/user.avsc -o gen/cabal exec wireform-gen -- thrift -i service.thrift -o gen/Then add gen to hs-source-dirs and list modules in your .cabal.
Supported schema languages: proto, avro, thrift, bond, capnp,
fbs, asn1, xsd.
Step 6: Go deeper
Section titled “Step 6: Go deeper”Once basic encode/decode works, these are the modules you reach for:
| Goal | Module |
|---|---|
| Stream protobuf messages over a socket | Proto.Decode.Stream |
| Stream MessagePack values | MsgPack.Stream |
| Read Avro container files | Avro.Container |
| Avro schema evolution | Avro.Resolution |
| gRPC framing (without full server) | Proto.GRPC |
| Full gRPC client/server | wireform-grpc package |
| Thrift RPC headers | Thrift.Message |
| MsgPack RPC | MsgPack.RPC |
| Dynamic protobuf (no generated types) | Proto.Dynamic |
| Protobuf text format | Proto.TextFormat |
| CBOR diagnostic dumps | CBOR.Diagnostic |
| CSS selectors on HTML | HTML.Selector |
| XPath queries on XML | XML.Path |
| XSLT transforms | XML.XSLT |
| SAX events from XML | XML.SAX |
| Concurrent XML parsing | XML.Incremental |
| Parquet read/write | Parquet.Read / Parquet.Write |
| Iceberg table metadata | Iceberg.JSON / Iceberg.Read |
| ORC columns | ORC.Read |
| Arrow IPC | Arrow.IPC |
| Kafka produce/consume | Kafka.Client.Producer / Kafka.Client.Consumer |
| Kafka Streams DSL | Kafka.Streams |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
loadProto cannot find file | Wrong cwd or path | Paths are relative to package root; cabal run from there. |
| Proto import not found | Missing include dir | loIncludeDirs in loadProtoWith, or -I for wireform-gen proto. |
Could not find module 'Proto.…' | Generated code not wired | Add gen to hs-source-dirs; list modules in .cabal. |
| Link errors / missing C symbols | No C compiler | Install gcc/clang; on macOS, Xcode CLI tools. |
| Bounds / solver failures | Version mismatch | Match base/text to your GHC; or use the path dependency. |