Skip to content

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”
ApproachWhen 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=DIROrganizations 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.

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")

For each message, the splice produces:

  • A Haskell record type with strict fields
  • MessageEncode / MessageSize / MessageDecode instances
  • ToJSON / FromJSON instances (proto3 canonical JSON)
  • Hashable, NFData, ProtoMessage instances
  • For enums: a sum type with an Unknown constructor for forward compatibility
FieldDefaultEffect
loIncludeDirs["proto/", "."]Search paths for imports
loFieldNamingPrefixedFieldsPrefixedFields or UnprefixedFields
loRepConfigdefaultRepConfigHow proto types map to Haskell types
loTHHooksnoneInject extra declarations per message

For projects where TH is not desirable:

Terminal window
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:

OptionDefaultEffect
genModulePrefix"Proto.Gen"Haskell module namespace
genFieldNamingPrefixedFieldsField naming convention
genStrictFieldsTrueStrict fields (bang patterns)
genUnpackPrimsTrueUNPACK on numeric fields
genDeriveGenericTrueDerive Generic
genPackedRepeatedTrueUse packed encoding for repeated fields
genLazySubmessagesFalseLazy decode of nested messages

The Proto umbrella module re-exports the primary API:

import Proto
let bytes = encodeMessage myMessage
case decodeMessage bytes of
Right msg -> use msg
Left err -> handleError err

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:

FunctionUse case
encodeMessageLazyLazy ByteString
hPutMessageWrite directly to a handle
hPutMessageLenLength-prefixed framing
buildMessageFramedgRPC-style length-delimited framing

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.

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 ''Event

Every field needs an explicit tag. The deriver supports Maybe fields, repeated fields (Vector, []), Map, oneofs, enums, and wire overrides like wireOverride WireZigZag.

Proto fields map to Haskell types through configurable adapters in Proto.Repr. The defaults are:

Proto typeDefault Haskell type
stringstrict Text
bytesstrict ByteString
repeated TVector 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.

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 myDynamicMessage
case decodeDynamic schema bytes of
Right msg -> print (dynamicField "name" msg)
Left err -> handleError err

For better decode performance, compile a ParseTable from the schema once and reuse it across many decodes with decodeDynamicWithSchema.

Proto.TextFormat reads and writes the protobuf text format (.pbtxt):

import Proto.TextFormat
let text = typedToTextPretty (Proxy @MyMessage) myMsg
case textToDynamic schema text of
Right dynMsg -> use dynMsg
Left err -> handleError err

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 -> unknownType

discoverRegistry is a TH splice that scans all imported modules for IsMessage instances and builds the registry automatically:

myRegistry :: TypeRegistry
myRegistry = $(discoverRegistry)

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:

TypeUtil moduleKey helpers
TimestampTimestamp.UtilRFC 3339 formatting, getCurrentTimestamp
DurationDuration.UtilArithmetic, conversion to/from seconds
AnyAny.UtilpackAny, unpackAny with TypeRegistry
FieldMaskFieldMask.UtilPath operations, merging
StructStruct.UtilConversion to/from Aeson Value
WrappersWrappers.UtilInt32Value, StringValue, etc.

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.

Proto2 extensions are supported via Proto.Extension:

import Proto.Extension
let val = getExtension myExtField msg
let msg' = setExtension myExtField val msg

Extensions are carried as unknown fields in the wire format and decoded on access.

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.

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.

wireform-proto vs proto-lens (decode) wireform-proto vs proto-lens (decode) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 625 1250 1875 2500 41.6 81.6 111 212 81.0 151 1422 2293 Small Medium Nested Repeated wireform-proto proto-lens wireform-proto vs proto-lens (decode) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 625 1250 1875 2500 41.6 81.6 111 212 81.0 151 1422 2293 Small Medium Nested Repeated wireform-proto proto-lens
Operationwireform-protoproto-lensratio
Small41.6 ns81.6 ns1.96x
Medium111 ns212 ns1.90x
Nested80.10 ns151 ns1.87x
Repeated1422 ns2293 ns1.61x

Last run 2026-06-27 11:56:42 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5.

wireform-proto vs proto-lens (encode, builder path) wireform-proto vs proto-lens (encode, builder path) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 1250 2500 3750 5000 26.6 151 72.4 286 55.5 333 1038 2758 Small Medium Nested Repeated wireform-proto proto-lens wireform-proto vs proto-lens (encode, builder path) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 1250 2500 3750 5000 26.6 151 72.4 286 55.5 333 1038 2758 Small Medium Nested Repeated wireform-proto proto-lens
Operationwireform-protoproto-lensratio
Small26.6 ns151 ns5.67x
Medium72.4 ns286 ns3.95x
Nested55.5 ns333 ns6.00x
Repeated1038 ns2758 ns2.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.