API reference

August 2, 2026 · View on GitHub

Complete signatures for the public Clojure API and the Java Writer / Reader surface most Clojure users will touch.

s-exp.hako

encode

(encode value)          ; -> byte[]
(encode value opts)     ; -> byte[]

Encode value and return a fresh byte[]. Opens a private confined arena, closes it before returning. Options — see Getting started and Performance:

OptionDefault
:initial-size256
:preserve-metafalse
:pack-homogeneousfalse
:coerce-custom-comparatorfalse

encode-to-segment

(encode-to-segment arena value)         ; -> MemorySegment
(encode-to-segment arena value opts)    ; -> MemorySegment

Encode into a segment allocated inside caller-supplied arena. The returned segment invalidates when arena closes. Options same as encode. See Arenas.

writer / encode-into! / encode-into-buffer!

(writer)                                 ; -> Writer  (initial-size = 4096)
(writer initial-size)                    ; -> Writer

(encode-into! wr value)                  ; -> MemorySegment (slice)
(encode-into! wr value opts)             ; -> MemorySegment (slice)

(encode-into-buffer! wr dst value)       ; -> byte count written
(encode-into-buffer! wr dst value opts)  ; -> byte count written

Reusable Writer. Writer implements AutoCloseable; wrap in with-open. The returned slice is valid until the next encode-into! call. Options same as encode.

encode-into-buffer! copies the encoded bytes directly into dst (a byte[] or ByteBuffer), skipping the ~40 B MemorySegment.asSlice wrapper that encode-into! returns. dst must be at least the encoded size; caller is responsible for sizing. Heap byte[] / ByteBuffer targets are zero-alloc; direct ByteBuffer targets allocate a MemorySegment.ofBuffer wrapper (~8 B). Returns the byte count.

encode-many / encode-many-into!

(encode-many values)                     ; -> byte[]
(encode-many values opts)                ; -> byte[]

(encode-many-into! wr values)            ; -> MemorySegment (slice)
(encode-many-into! wr values opts)       ; -> MemorySegment (slice)

Encode many values with one shared symbol table. Single envelope, concatenated encoded values. Options same as encode, plus opts carry through to each value.

encode-many-into! uses a reusable Writer — skips the ~500 B per-call Writer construction that encode-many pays. Same slice lifecycle as encode-into!.

decode

(decode src)                             ; -> value
(decode src opts)                        ; -> value

src may be a byte[] or a MemorySegment. Options:

OptionDefault
:zero-copyfalse
:tolerate-unknown-tagsfalse
:cache-identsfalse

decode-many

(decode-many src)                        ; -> vector of values
(decode-many src opts)                   ; -> vector of values

Convenience: equivalent to (into [] (decoder src opts)).

decoder

(decoder src)                            ; -> reducible + iterable
(decoder src opts)                       ; -> reducible + iterable

Returns a value-stream source that implements both clojure.lang.IReduceInit and Iterable. Composes with any standard clojure.core fn that consumes reducibles or iterables:

(into #{} (filter :active) (hako/decoder bs))
(sequence (map :id) (hako/decoder bs))
(eduction  xform (hako/decoder bs))
(reduce f init (hako/decoder bs))
(transduce xform f init (hako/decoder bs))

Each reduction / iteration spins up a fresh Reader; the source is safe to walk multiple times. reduced terminates the reader immediately. Options same as decode.

reader / decode-into!

(reader src)                             ; -> Reader

(decode-into! rd src)                    ; -> value
(decode-into! rd src opts)               ; -> value

Reusable Reader. .reset(newSeg) rebinds internally. Options same as decode.

encode-pooled / decode-pooled / close-thread-locals!

(encode-pooled value)                    ; -> byte[]
(encode-pooled value opts)               ; -> byte[]

(decode-pooled src)                      ; -> value
(decode-pooled src opts)                 ; -> value

(close-thread-locals!)                   ; -> nil

Opt-in ThreadLocal-backed variants of encode / decode. Skip the per-call Writer / Reader construction (~500 B / ~250 B). Same options + return shape as the non-pooled versions.

Cleanup: on short-lived threads or in servlet containers, call close-thread-locals! at thread exit / request boundary to release the pooled Writer's confined Arena and drop scratch buffers. Long-lived pooled worker threads never need cleanup — the pool amortises setup cost over the thread lifetime. Long-strings decode uses a 1 MiB scratch cap (above that, one-shot alloc) so a pathological large-string decode won't pin memory.

s-exp.hako.ext

TaggedValue

(->TaggedValue ext-id memory-segment)   ; deftype
(tagged-value? x)                        ; -> boolean

Returned by tolerant decode when an unregistered user-tag is seen. :ext is the u32 tag id; :bytes is a MemorySegment slice of the raw payload.

register-record!

(register-record! record-class)          ; -> record-class

Register a Clojure defrecord or Java record class. Reflects once, caches a MethodHandle for the canonical constructor. Registered classes participate in encode / decode automatically.

register-user-tag!

(register-user-tag! id klass write-fn read-fn)         ; -> wire-id
(register-user-tag-raw! wire-id klass write-fn read-fn) ; -> wire-id

Bind a user-tag id to a Java class + encode/decode callbacks.

  • register-user-tag! accepts a small app-local id (0..0x0FFFFFFF) and shifts it into the private wire range (0x10000000+). Callers write 1, 2, ... instead of hex constants. Returns the full wire id (a long).
  • register-user-tag-raw! accepts a full u32 wire id — escape hatch for cross-app coordination or public-range registrations. See ../EXTENSIONS.md §E.2 for the range map.
  • write-fn(fn [^Writer w value]) — write the payload bytes.
  • read-fn(fn [^Reader r]) — parse one value from the length-bounded payload region.

default-comparator?

(default-comparator? sorted-coll)        ; -> boolean

true if sorted-coll uses hako's known natural-ordering comparator (the one sorted-set / sorted-map install). Used internally to detect custom comparators.

Java Writer

Namespace: com.s_exp.hako.Writer.

Constructor and lifecycle:

SignatureNotes
new Writer(long initialSize)Opens Arena.ofConfined().
close()Closes the arena. AutoCloseable.
reset()Cursor to 0. Preserves handler + opts.
finish()Returns a slice [0, pos).

Primitive emitters (mostly consumed by user-tag write-fns):

MethodWire effect
putByte(int)1 byte.
putU16(int)2 bytes LE.
putU32(long)4 bytes LE.
putI32(int)4 bytes LE signed.
putU64(long)8 bytes LE.
putF32(float) / putF64(double)4 / 8 bytes LE.
putBytes(byte[])Raw copy.
putTierValue(long)Tier code byte + payload.
putSizedTag(int major, long count)Full tag + tier.

Scalar writers:

Method
writeNil(), writeTrue(), writeFalse()
writeLong(long), writeDouble(double)
writeFloat(float), writeChar(int)
writeString(String), writeBytes(byte[])
writeUuid(long msb, long lsb)
writeInstant(long epochSec, int nano)
writeBigInteger(BigInteger)
writeBigDecimal(BigDecimal)
writeRatio(clojure.lang.Ratio)
writeLongArray(long[]) / writeDoubleArray(double[])
writeIntArray(int[]) / writeFloatArray(float[])

Container headers (write value payloads yourself):

  • writeVectorHeader(long n)
  • writeListHeader(long n)
  • writeSetHeader(long n)
  • writeMapHeader(long n)

Composite:

  • writeInterned(int major, Object internKey, String ns, String name)
  • writeEnvelope()
  • writeRecord(Object v) — full record write.
  • writeAny(Object v) — top-level dispatch.
  • beginUserTag(int tagId)long mark; endUserTag(long mark).

Config setters (preserved across reset()):

  • setWriteMeta(boolean), setPackHomogeneous(boolean), setCoerceCustomComparator(boolean)
  • setUnknownHandler(UnknownHandler)

Java Reader

Namespace: com.s_exp.hako.Reader.

Constructor and lifecycle:

Signature
new Reader(MemorySegment)
reset(MemorySegment newSeg)
pos(), remaining(), segment()

Primitive readers:

MethodReturns
getByte()int (0..255)
getU16()int (0..65535)
getU32()long (0..2322^{32}-1)
getI32()int
getI64()long
getF32(), getF64()float / double
getBytes(int n)fresh byte[]
sliceBytes(long n)zero-copy MemorySegment
getString(int n)UTF-8 decoded String
readTierPayload(int tierCode)long
readTierValue()tier-code byte + payload

Bulk:

  • readLongArray(int), readDoubleArray(int)
  • readIntArray(int), readFloatArray(int)

Composite:

  • readEnvelope() — checks magic + version, advances 5 bytes.
  • readAny() — top-level dispatch, returns Clojure-friendly value.
  • skip(long n) — advance cursor without reading.
  • internAdd(Object), internGet(int) — per-message sym-table.

Config setters (preserved across reset()):

  • setZeroCopy(boolean), setTolerant(boolean), setCacheIdents(boolean)
  • setArrayMapThresholds(int nonKw, int kw)
  • setExtensionHandler(ExtensionHandler)