A bitpacking serialization library for Java. Part of the serialize family, wire compatible with the C++, C, Go, C#, Rust, JavaScript, Dart and Elixir libraries — the same values produce the same bytes in every implementation, so a stream written by one reads in any other. STANDARD.md — a verbatim vendored copy of the specification in mas-bandwidth/serialize, which CI checks for drift — is the authority on every byte.
Version 1.1.2 (SerializeUtil.VERSION).
serialize.java ships as source today. The implementation is ready and the package is not yet published: there is no artifact on Maven Central under any coordinate, and there is no Maven or Gradle build here to produce one. Publishing it is a separate round.
Sixteen files under src/serialize/, zero dependencies, Java 17 language
level. Take it either way:
Copy the package in — drop src/serialize/ into your own source tree and
it compiles with the rest of it, no build changes at all.
Build a jar — the same javac line the Makefile uses, against any JDK 17 or newer:
git clone https://github.com/mas-bandwidth/serialize.java.git
javac --release 17 -d classes serialize.java/src/serialize/*.java
jar cf serialize.jar -C classes serializethen compile and run against it with -cp serialize.jar. Write-side contracts
are assert statements, so run with -ea while developing and without it in
release — the two shapes the test suite covers.
Pin a release tag rather than tracking main. The newest is on the
releases page: a
release states a format version, and two endpoints interoperate only when they
carry the same one.
One package, serialize, zero dependencies. The complete family
operation set on three streams — WriteStream, ReadStream and
MeasureStream — sharing the BitStream interface, so a single
serialize function writes, reads and measures. Values travel in
primitive-specialized holder cells (IntRef, LongRef, BoolRef,
FloatRef, DoubleRef) that keep the hot path free of boxing, and in
Ref<T> for object values (String, the 128-bit pair).
USAGE.md teaches every operation by example.
- Raw bits:
serializeBits(1–32),serializeBits64(1–64),serializeAlign. - Ranged integers:
serializeInt,serializeInt64,serializeInt128— offset from min in exactly the bit length of the range, unsigned-domain arithmetic so ranges wider than 2^63/2^127 are exact, zero bits for a degeneratemin == maxrange on every width. - Unsigned helpers and bool:
serializeUint8/16/32/64,serializeUint128(theUInt128Valuepair),serializeBool. - Floats:
serializeFloatandserializeDouble, bit transparent both ways — every pattern legal, NaN payloads ride byte-for-byte;serializeCompressedFloat, quantizing in float32 with the standard's two roundings on each side. - Bytes and strings:
serializeBytes(aligned bulk copy, count agreed, not transmitted);serializeString(UTF-8 on the wire, payload validated on read in every mode);serializeWideString(one 32-bit group per UTF-16 code unit, no alignment anywhere). - The relative integer:
serializeIntRelative— the flag ladder for strictly increasing sequences over the domain 0 to 2^31 - 1, one bit for a difference of 1, every tier's reconstruction checked on read. - Fixed point:
serializeFixedat 8/16/32/64-bit storage andserializeFixed128at 128-bit storage — Q formats, the raw scaled integer as an exact ranged offset, byte identical toserializeInt64wherever storage fits 64 bits. - 128-bit values:
Int128Value/UInt128Value, two's complement pairs of long halves, mirroring the family's emulated pair types. - Utilities:
SerializeUtil—bitsRequired/64/128, zig-zag conversion, the compressed-float step count. - Composition:
serializeObject, which runs a nestedSerializerinline and contributes no bytes of its own — no framing, no length prefix, no alignment inserted around it. - The bitpacker underneath:
BitWriterandBitReader, the family wire in branchless 64-bit window loads. Every stream and both bitpackers exposereset(...)for allocation-free reuse.
import serialize.*;
final class Player
{
final IntRef health = new IntRef();
final BoolRef alive = new BoolRef();
final FloatRef heading = new FloatRef();
}
static boolean serializePlayer( BitStream stream, Player player )
{
return stream.serializeInt( player.health, 0, 100 )
&& stream.serializeBool( player.alive )
&& stream.serializeFloat( player.heading );
}
byte[] buffer = new byte[64]; // length a multiple of 8
WriteStream writer = new WriteStream( buffer, buffer.length );
serializePlayer( writer, player ); // -> true
writer.flush(); // always flush before touching the bytes
int packetBytes = (int) writer.getBytesProcessed(); // 5 bytes: 7 + 1 + 32 bits
ReadStream reader = new ReadStream( buffer, packetBytes );
serializePlayer( reader, decoded ); // -> trueThe JDK is pinned per project, not taken from the system:
tending/PINS.md records the exact version, download
URL and SHA-256. dist/ is gitignored — re-fetch by the pinned URL,
verify the hash, and unpack so the JDK sits at
dist/jdk-21.0.12.1/Contents/Home. The library targets the Java 17
language level (javac --release 17), built and tested on the pinned
JDK 21. A plain Makefile drives everything — no Maven, no Gradle:
make # both shapes below
make test # the suite with assertions on, the checked shape
make test-release # the same suite with assertions off, the release shape
make test runs the suite with assertions enabled (-ea): writer
contracts are assert statements, so this is the checked shape.
make test-release runs the same suite with assertions disabled — the
release shape, where asserts compile to nothing at runtime, matching the
C++ library's serialize_assert under NDEBUG — which is what proves
the read side's refusals are checks rather than asserts. Both are CI
gates.
The suite runs every vector in conformance/, the
family's shared corpus, vendored from mas-bandwidth/serialize and
checked for drift by CI. The directory is discovered at run time rather
than named in the source, and a vector whose operation the runner cannot
drive fails rather than being skipped. An accepted vector must decode to
the stated value and consume the stated bits; a vector marked
writer = canonical is re-emitted through the write stream and compared
byte for byte, flush included; a vector carrying measure_at_least is
held to that floor on the measure stream. A refused vector must be
refused, must leave the caller's scalar destination unwritten, and must
leave the stream terminal, which is checked by behavior: every later step
refuses too, and a further read fails, consumes no bits and writes
nothing. Nothing regenerates its own expectations. The suite also pins
the family's golden vectors byte for byte — the golden wire message
covering every operation class, the discriminating compressed-float vectors (bit
patterns, not tolerances), the string and wide-string pins, every
relative-integer tier, and the fixed point shapes at every group count —
plus a sabotage sweep proving every consumed bit of the golden stream is
load bearing, refusal and terminality proofs for hostile input, and the
measure bound.
interop/ takes it further: the CI interop job builds the
C++ reference at a pinned release and runs it head to head with this
port. Both halves write the same boundary message — every operation the
standard defines, at its boundary values — and the files must be byte
identical; each then decodes the other's bytes and re-encodes them
exactly; both must refuse every truncation of the other's stream; and
both run the corpus. The release candidate in this repository exchanges
bytes with the reference on every push, so wire compatibility is
measured rather than asserted. make interop MODE=write FILE=out.bin
runs one exchange by hand.
Benchmarking for the serialize family lives in mas-bandwidth/schema's data-driven bench, which measures the generated codecs across every language on one corpus.
BSD 3-Clause, © Más Bandwidth LLC.