Protocol Buffers
wireform-proto implements Protocol Buffers from the .proto IDL down to the
wire. It includes a parser, a code generator, Template Haskell splicing,
proto3 JSON mapping, text format, well-known types, extensions, dynamic
messages, and a type registry. It is the largest and oldest package in the
wireform ecosystem.
Three ways to get Haskell types from .proto files
Section titled “Three ways to get Haskell types from .proto files”| Approach | When to use |
|---|---|
$(loadProto "file.proto") | Small projects where TH is acceptable; types land in the same module |
wireform-gen proto -i file.proto -o gen/ | Larger projects; CI-friendly; commit generated code |
protoc --wireform_out=DIR | Organizations that standardize on protoc plugins |
All three produce the same output: record types with MessageEncode,
MessageDecode, and MessageSize instances, plus Aeson JSON instances and
ProtoMessage metadata.
Template Haskell splicing
Section titled “Template Haskell splicing”The fastest way to get started:
{-# LANGUAGE TemplateHaskell #-}module MyModule where
import Proto.TH (loadProto)
$(loadProto "proto/person.proto")This parses the .proto file at compile time and splices Haskell types into
your module. For files that import other .proto files, pass include
directories:
import Proto.TH (loadProtoWith, defaultLoadOpts, LoadOpts(..))
$(loadProtoWith defaultLoadOpts { loIncludeDirs = ["proto", "."] } "proto/api.proto")What gets generated
Section titled “What gets generated”For each message, the splice produces:
- A Haskell record type with strict fields
MessageEncode/MessageSize/MessageDecodeinstancesToJSON/FromJSONinstances (proto3 canonical JSON)Hashable,NFData,ProtoMessageinstances- For enums: a sum type with an
Unknownconstructor for forward compatibility
LoadOpts
Section titled “LoadOpts”| Field | Default | Effect |
|---|---|---|
loIncludeDirs | ["proto/", "."] | Search paths for imports |
loFieldNaming | PrefixedFields | PrefixedFields or UnprefixedFields |
loRepConfig | defaultRepConfig | How proto types map to Haskell types |
loTHHooks | none | Inject extra declarations per message |
Standalone code generation
Section titled “Standalone code generation”For projects where TH is not desirable:
cabal exec wireform-gen -- proto -i proto/person.proto -o gen/This writes .hs files to gen/. Add gen to hs-source-dirs in your
.cabal file and list the generated modules in exposed-modules or
other-modules.
The GenerateOpts type controls output:
| Option | Default | Effect |
|---|---|---|
genModulePrefix | "Proto.Gen" | Haskell module namespace |
genFieldNaming | PrefixedFields | Field naming convention |
genStrictFields | True | Strict fields (bang patterns) |
genUnpackPrims | True | UNPACK on numeric fields |
genDeriveGeneric | True | Derive Generic |
genPackedRepeated | True | Use packed encoding for repeated fields |
genLazySubmessages | False | Lazy decode of nested messages |
Encoding and decoding
Section titled “Encoding and decoding”The Proto umbrella module re-exports the primary API:
import Proto
let bytes = encodeMessage myMessagecase decodeMessage bytes of Right msg -> use msg Left err -> handleError errEncode
Section titled “Encode”encodeMessage does a two-pass encode: first messageSize computes the exact
byte count, then buildMessage writes into a pre-allocated buffer. This avoids
the intermediate chunk copies that a streaming Builder would produce.
For streaming or framed output:
| Function | Use case |
|---|---|
encodeMessageLazy | Lazy ByteString |
hPutMessage | Write directly to a handle |
hPutMessageLen | Length-prefixed framing |
buildMessageFramed | gRPC-style length-delimited framing |
Decode
Section titled “Decode”decodeMessage returns Either DecodeError a. The decoder uses unboxed sums
internally, so the success path allocates only the final Haskell value. Unknown
fields are captured and round-tripped if the type has HasExtensions.
Annotation-driven deriving
Section titled “Annotation-driven deriving”If you have hand-written Haskell types and want proto instances without a
.proto file, use Proto.TH.Derive:
import Proto.TH.Derive (deriveProto)import Wireform.Derive (tag, wireOverride, WireOverride(..))
data Event = Event { eventId :: !Int64 , eventName :: !Text , eventTime :: !Word64 }
{-# ANN eventId (tag 1) #-}{-# ANN eventName (tag 2) #-}{-# ANN eventTime (tag 3) #-}
deriveProto ''EventEvery field needs an explicit tag. The deriver supports Maybe fields,
repeated fields (Vector, []), Map, oneofs, enums, and wire overrides
like wireOverride WireZigZag.
Representation adapters
Section titled “Representation adapters”Proto fields map to Haskell types through configurable adapters in
Proto.Repr. The defaults are:
| Proto type | Default Haskell type |
|---|---|
string | strict Text |
bytes | strict ByteString |
repeated T | Vector T |
map<K,V> | Map K V (ordered) |
Override these via RepConfig:
import Proto.Repr
myRepConfig = defaultRepConfig { configDefault = defaultFieldRep { fieldRepeated = listAdapter -- use [] instead of Vector , fieldMap = hashMapAdapter -- use HashMap instead of Map } }
$(loadProtoWith defaultLoadOpts { loRepConfig = myRepConfig } "proto/api.proto")Available adapters include lazyTextAdapter, shortTextAdapter,
lazyBytesAdapter, shortBytesAdapter, unboxedVectorAdapter, seqAdapter,
and hashMapAdapter.
Dynamic messages
Section titled “Dynamic messages”When you don’t have generated types (e.g. processing arbitrary proto messages
at runtime), Proto.Dynamic gives you an untyped API:
import Proto.Dynamic
let bytes = encodeDynamic myDynamicMessagecase decodeDynamic schema bytes of Right msg -> print (dynamicField "name" msg) Left err -> handleError errFor better decode performance, compile a ParseTable from the schema once and
reuse it across many decodes with decodeDynamicWithSchema.
Text format
Section titled “Text format”Proto.TextFormat reads and writes the protobuf text format (.pbtxt):
import Proto.TextFormat
let text = typedToTextPretty (Proxy @MyMessage) myMsgcase textToDynamic schema text of Right dynMsg -> use dynMsg Left err -> handleError errType registry
Section titled “Type registry”Proto.Registry provides an explicit registry of message types for use with
Any packing/unpacking and dynamic message dispatch:
import Proto.Registry
let registry = emptyRegistry & registerMessage @MyMessage & registerMessage @OtherMessage
case lookupDecoder registry "type.googleapis.com/my.Message" of Just decoder -> decoder bytes Nothing -> unknownTypediscoverRegistry is a TH splice that scans all imported modules for
IsMessage instances and builds the registry automatically:
myRegistry :: TypeRegistrymyRegistry = $(discoverRegistry)Well-known types
Section titled “Well-known types”Proto.Google.Protobuf.* modules are code-generated from the upstream
.proto files in proto/google/protobuf/. Each well-known type has a
companion *.Util module with helper functions:
| Type | Util module | Key helpers |
|---|---|---|
Timestamp | Timestamp.Util | RFC 3339 formatting, getCurrentTimestamp |
Duration | Duration.Util | Arithmetic, conversion to/from seconds |
Any | Any.Util | packAny, unpackAny with TypeRegistry |
FieldMask | FieldMask.Util | Path operations, merging |
Struct | Struct.Util | Conversion to/from Aeson Value |
Wrappers | Wrappers.Util | Int32Value, StringValue, etc. |
Proto3 JSON
Section titled “Proto3 JSON”The Proto.Internal.JSON modules implement the proto3 canonical JSON mapping.
The ToJSON/FromJSON instances generated by loadProto and wireform-gen
use this mapping automatically. It handles field name conversion (proto
snake_case to JSON camelCase), default value omission, Any type URLs,
well-known type special encodings, and NullValue.
Extensions (proto2)
Section titled “Extensions (proto2)”Proto2 extensions are supported via Proto.Extension:
import Proto.Extension
let val = getExtension myExtField msglet msg' = setExtension myExtField val msgExtensions are carried as unknown fields in the wire format and decoded on access.
Conformance
Section titled “Conformance”wireform-proto passes 2,675 / 2,675 tests in the official protobuf
conformance suite, covering proto2 and proto3 binary encoding, proto3 JSON,
and text format.
Performance
Section titled “Performance”wireform-proto is 3-7x faster than proto-lens on both encode and decode. The speedup comes from unboxed sums in the decoder, direct-write encoding, and inlined field codecs.
Decode: wireform-proto vs proto-lens
Section titled “Decode: wireform-proto vs proto-lens”| Operation | wireform-proto | proto-lens | ratio |
|---|---|---|---|
| Small | 41.6 ns | 81.6 ns | 1.96x |
| Medium | 111 ns | 212 ns | 1.90x |
| Nested | 80.10 ns | 151 ns | 1.87x |
| Repeated | 1422 ns | 2293 ns | 1.61x |
Last run 2026-06-27 11:56:42 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5.
Encode: wireform-proto vs proto-lens
Section titled “Encode: wireform-proto vs proto-lens”| Operation | wireform-proto | proto-lens | ratio |
|---|---|---|---|
| Small | 26.6 ns | 151 ns | 5.67x |
| Medium | 72.4 ns | 286 ns | 3.95x |
| Nested | 55.5 ns | 333 ns | 6.00x |
| Repeated | 1038 ns | 2758 ns | 2.66x |
Last run 2026-06-27 11:56:42 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5.
The charts and tables above are regenerated by wireform-stats from wireform-proto/bench-results/summary/proto-vs-proto-lens-{decode,encode}.json — the same source the README charts are built from.