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:
| Option | Default |
|---|---|
:initial-size | 256 |
:preserve-meta | false |
:pack-homogeneous | false |
:coerce-custom-comparator | false |
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:
| Option | Default |
|---|---|
:zero-copy | false |
:tolerate-unknown-tags | false |
:cache-idents | false |
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-localid(0..0x0FFFFFFF) and shifts it into the private wire range (0x10000000+). Callers write1,2, ... instead of hex constants. Returns the full wire id (along).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:
| Signature | Notes |
|---|---|
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):
| Method | Wire 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:
| Method | Returns |
|---|---|
getByte() | int (0..255) |
getU16() | int (0..65535) |
getU32() | long (0..-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)