Skip to content

wireform-fory

wireform-fory implements Apache Fory (formerly Apache Fury), a cross-language serialization format optimized for RPC and data exchange between JVM, Python, and other runtimes. Fory supports reference tracking, meta-string compression, schema-hashed named structs, and chunked collections. Use this package when you need wire-compatible payloads with Python services using pyfory 0.17, or when shared subgraphs and large string tables make reference tracking worthwhile.

Fory is more configuration-heavy than CBOR or MessagePack. Encoder options, struct registries, and schema registration affect the on-wire layout.

  • Template Haskell deriving via deriveFory for records and algebraic types, with wireform-derive annotations; Generic defaults (empty instances) work for simple cases
  • Reference tracking to deduplicate shared objects and cyclic graphs on the wire
  • Meta-string compression for repeated field and type names
  • Named structs with schema hash for pyfory-compatible NAMED_STRUCT layout
  • Chunked collections for lists, sets, and maps with homogeneous element types
  • One-dimensional primitive arrays (BoolArray, Int32Array, …) with byte-identical layouts to pyfory’s NumPy serializer
  • Wire compatibility with pyfory 0.17 for the supported type set

Derive Fory codecs with the Template Haskell deriver and round-trip through the default encoder:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
module Event where
import Fory.Class (ToFory, FromFory, encodeFory, decodeFory)
import Fory.Derive (deriveFory)
import GHC.Generics (Generic)
import Data.Text (Text)
data Event = Event
{ eventId :: !Int64
, eventName :: !Text
}
deriving stock (Show, Eq, Generic)
$(deriveFory ''Event)
send :: Event -> ByteString
send ev = encodeFory ev
receive :: ByteString -> Either String Event
receive bs = decodeFory bs

For simple cases with no wire-format customization, Generic defaults also work: add deriving Generic and declare empty instance ToFory Event and instance FromFory Event declarations.

When the same object appears more than once in a graph, enable reference tracking so subsequent occurrences encode as back-references:

import Fory.Class (toFory)
import Fory.Encode (encodeWith)
import Fory.Options qualified as O
encodeWithRefs :: Event -> ByteString
encodeWithRefs ev =
encodeWith (O.defaultEncodeOptions { O.eoRefTracking = True }) (toFory ev)

For pyfory-compatible named structs, register schemas in the encoder options so the wire layout includes the 4-byte fingerprint hash:

import Fory.Options qualified as O
import Fory.Struct (StructSchema, mkSchema)
import Fory.TypeId (INT32, STRING)
personSchema :: StructSchema
personSchema =
mkSchema "myapp" "Person"
[ ("name", STRING)
, ("age", INT32)
]
encodePersonOpts :: O.EncodeOptions
encodePersonOpts =
O.defaultEncodeOptions
{ O.eoStructRegistry = O.registerStruct personSchema O.emptyStructRegistry
}

Use the primitive array newtypes when exchanging numeric buffers with Python NumPy code:

import Fory.Class (Int32Array(..), ToFory, FromFory, encodeFory, decodeFory)
import qualified Data.Vector.Storable as VS
timeseries :: VS.Vector Int32 -> ByteString
timeseries vec = encodeFory (Int32Array vec)
readTimeseries :: ByteString -> Either String (VS.Vector Int32)
readTimeseries bs = do
Int32Array vec <- decodeFory bs
pure vec

Encode/decode across representative shapes

Section titled “Encode/decode across representative shapes”
wireform-fory encode + decode across representative shapes wireform-fory encode + decode across representative shapes lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 5000 10000 15000 20000 75.6 62.5 85.3 71.8 127 61.0 266 440 6630 10783 int string bytes 1KB Person struct list[Person]*100 encode decode wireform-fory encode + decode across representative shapes lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 5000 10000 15000 20000 75.6 62.5 85.3 71.8 127 61.0 266 440 6630 10783 int string bytes 1KB Person struct list[Person]*100 encode decode
Operationencodedecoderatio
int75.7 ns62.5 ns0.83x
string85.3 ns71.8 ns0.84x
bytes 1KB127 ns60.10 ns0.48x
Person struct266 ns440 ns1.65x
list[Person]*1006630 ns10783 ns1.63x

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

Scalar encode/decode runs under 130 ns. Struct payloads are sub-microsecond. The 100-element list benchmark shows ~67 ns per element on encode and ~107 ns per element on decode, competitive with Fory implementations in other languages.

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

ModulePurpose
Fory.ClassToFory / FromFory, primitive array newtypes, Shared wrapper
Fory.Encode / Fory.DecodePure encode and decode entry points
Fory.IOIn-place buffer encoder with ref and meta-string pools
Fory.OptionsEncodeOptions / DecodeOptions, struct registry
Fory.StructStructSchema definitions for named struct wire layout
Fory.ValueDynamic untyped value ADT
Fory.MetaStringMeta-string compression tables and encodings
Fory.TypeIdWire type identifiers
Fory.DeriveAnnotation-driven deriver with field renaming support

The package is verified against pyfory 0.17 for null, booleans, integers, floats, strings, binary, chunked lists/sets/maps, named structs with registered schemas, primitive arrays, reference tracking, and meta-string compression. Cross-language interop for NAMED_COMPATIBLE_STRUCT (schema evolution) is still in progress.