AGON: Alternative General Object Notation

with Adhiraj Singh • Theory of Computation • Spring 2021

Built a binary data interchange format that is 70% faster at encoding and produces 71% smaller files than JSON. Compared against V8's serialisation module, AGON generated files 0.14% smaller for our test payloads.

Approach

JSON prioritises human readability, but this comes at a cost in size and bandwidth. We designed AGON to store data in a custom binary format with a formal grammar specification, then built an encoder and decoder in TypeScript.

The format uses binary markers for primitive data types and start/end markers around non-primitive structures. This eliminates the overhead of string-based encoding while maintaining interoperability with JSON data structures.

The Grammar

Our encoder and decoder adhere to a context-free grammar that covers objects, arrays, numbers, booleans, and strings. The grammar has no left-recursion, and each production rule starts with a unique binary marker, removing the need for look-ahead when parsing.

<S>   ::= <V>
<V>   ::= <O> | <A> | <N> | <B> | <Str>
<N>   ::= <I> | <F>
<I>   ::= t0 <T> | t1 <TT> | t2 <TTTT> | t3 <TTTTTTTT>
<F>   ::= t4 <TTTT> | t5 <TTTTTTTT>
<B>   ::= t6 | t7
<A>   ::= t8 <Ai> t9
<Ai>  ::= <V> | <Ai> <V> | ε
<O>   ::= t10 <Oi> t11
<Oi>  ::= <str> <V> | <Oi> <str> <V> | ε
<str> ::= t12 <P> 0
<P>   ::= <T0> | <P> <T0> | ε
<T0>  ::= 0x1 | 0x2 | ... | 0xFF
<T>   ::= 0x0 | <T0>

Terminal Markers

We use 16 different terminals to represent data types. Each marker is a single byte:

arrayStart:    0xF0    arrayEnd:      0xF1
objectStart:   0xF2    objectEnd:     0xF3
byte:          0xFA    short:         0xFB
int:           0xFC    long:          0xFD
double:        0xFE    float:         0xFF
booleanTrue:   0xF4    booleanFalse:  0xF5
string:        0xF6    buffer:        0xF7
date:          0xEF    null:          0xD0

Encoder

The encoder converts JavaScript objects, arrays, and primitives to binary format. Key design decisions:

  • Booleans: Single byte (0xF4 or 0xF5) instead of 4-5 bytes for "true"/"false"
  • Numbers: Type inference determines the smallest C-like type (byte, short, int, long) that can hold the value
  • Strings: UTF-16 encoded with null termination
  • Objects/Arrays: Start/end markers with recursive encoding of contents

Decoder

The decoder uses a reverse terminal map to identify markers and a stack to track nested structures. When it encounters an objectStart or arrayStart, it pushes to the stack and creates the appropriate container. Matching end markers pop from the stack.

For numbers, we use a DataView to handle binary conversions according to the platform's endianness automatically.

Benchmarks

We ran 15 benchmark rounds, each generating 15 random objects (roughly 50% arbitrarily nested between 1-20 levels), totalling 225 test objects.

Encoding speed: AGON completed typical serialisation in 16.24 ms versus JSON's 55.08 ms, a 70.5% improvement.

File size: JSON files averaged 2320 bytes; AGON files averaged 667 bytes, a 71.3% reduction. AGON files were also 0.14% smaller than V8's serialisation output.

Decoding was slower than JSON in our implementation, which we identified as an area for future optimisation.

Links