json-as
Version:
The only JSON library you'll need for AssemblyScript with SIMD and SWAR
491 lines (349 loc) • 13.7 kB
Markdown
<h1 align="center"><pre> ╦╔═╗╔═╗╔╗╔ ╔═╗╔═╗
║╚═╗║ ║║║║══╠═╣╚═╗
╚╝╚═╝╚═╝╝╚╝ ╩ ╩╚═╝</pre></h1>
<details>
<summary>Table of Contents</summary>
- [Installation](#installation)
- [Usage](#usage)
- [Examples](#examples)
- [Omitting Fields](#omitting-fields)
- [Using Nullable Primitives](#using-nullable-primitives)
- [Working with Unknown or Dynamic Data](#working-with-unknown-or-dynamic-data)
- [Using Raw JSON Strings](#using-raw-json-strings)
- [Working with Enums](#working-with-enums)
- [Using Custom Serializers or Deserializers](#using-custom-serializers-or-deserializers)
- [Performance](#performance)
- [Comparison to JavaScript](#comparison-to-javascript)
- [Performance Tuning](#performance-tuning)
- [Running Benchmarks Locally](#running-benchmarks-locally)
- [Debugging](#debugging)
- [License](#license)
- [Contact](#contact)
</details>
## Installation
```bash
npm install json-as
```
Add the `--transform` to your `asc` command (e.g. in package.json)
```bash
--transform json-as/transform
```
Optionally, for additional performance, also add:
```bash
--enable simd
```
Alternatively, add it to your `asconfig.json`
```typescript
{
"options": {
"transform": ["json-as/transform"]
}
}
```
If you'd like to see the code that the transform generates, run the build step with `DEBUG=true`
## Usage
```typescript
import { JSON } from "json-as";
@json
class Vec3 {
x: f32 = 0.0;
y: f32 = 0.0;
z: f32 = 0.0;
}
@json
class Player {
@alias("first name")
firstName!: string;
lastName!: string;
lastActive!: i32[];
// Drop in a code block, function, or expression that evaluates to a boolean
@omitif((self: Player) => self.age < 18)
age!: i32;
@omitnull()
pos!: Vec3 | null;
isVerified!: boolean;
}
const player: Player = {
firstName: "Jairus",
lastName: "Tanaka",
lastActive: [3, 9, 2025],
age: 18,
pos: {
x: 3.4,
y: 1.2,
z: 8.3,
},
isVerified: true,
};
const serialized = JSON.stringify<Player>(player);
const deserialized = JSON.parse<Player>(serialized);
console.log("Serialized " + serialized);
console.log("Deserialized " + JSON.stringify(deserialized));
```
## Examples
### Omitting Fields
This library allows selective omission of fields during serialization using the following decorators:
**@omit**
This decorator excludes a field from serialization entirely.
```typescript
@json
class Example {
name!: string;
@omit
SSN!: string;
}
const obj = new Example();
obj.name = "Jairus";
obj.SSN = "123-45-6789";
console.log(JSON.stringify(obj)); // { "name": "Jairus" }
```
**@omitnull**
This decorator omits a field only if its value is null.
```typescript
@json
class Example {
name!: string;
@omitnull()
optionalField!: string | null;
}
const obj = new Example();
obj.name = "Jairus";
obj.optionalField = null;
console.log(JSON.stringify(obj)); // { "name": "Jairus" }
```
**@omitif((self: this) => condition)**
This decorator omits a field based on a custom predicate function.
```typescript
@json
class Example {
name!: string;
@omitif((self: Example) => self.age <= 18)
age!: number;
}
const obj = new Example();
obj.name = "Jairus";
obj.age = 18;
console.log(JSON.stringify(obj)); // { "name": "Jairus" }
obj.age = 99;
console.log(JSON.stringify(obj)); // { "name": "Jairus", "age": 99 }
```
If age were higher than 18, it would be included in the serialization.
### Using nullable primitives
AssemblyScript doesn't support using nullable primitive types, so instead, json-as offers the `JSON.Box` class to remedy it.
For example, this schema won't compile in AssemblyScript:
```typescript
@json
class Person {
name!: string;
age: i32 | null = null;
}
```
Instead, use `JSON.Box` to allow nullable primitives:
```typescript
@json
class Person {
name: string;
age: JSON.Box<i32> | null = null;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("Jairus");
console.log(JSON.stringify(person)); // {"name":"Jairus","age":null}
person.age = new JSON.Box<i32>(18); // Set age to 18
console.log(JSON.stringify(person)); // {"name":"Jairus","age":18}
```
### Working with unknown or dynamic data
Sometimes it's necessary to work with unknown data or data with dynamic types.
Because AssemblyScript is a statically-typed language, that typically isn't allowed, so json-as provides the `JSON.Value` and `JSON.Obj` types.
Here's a few examples:
**Working with multi-type arrays**
When dealing with arrays that have multiple types within them, eg. `["string",true,["array"]]`, use `JSON.Value[]`
```typescript
const a = JSON.parse<JSON.Value[]>('["string",true,["array"]]');
console.log(JSON.stringify(a[0])); // "string"
console.log(JSON.stringify(a[1])); // true
console.log(JSON.stringify(a[2])); // ["array"]
```
**Working with unknown objects**
When dealing with an object with an unknown structure, use the `JSON.Obj` type
```typescript
const obj = JSON.parse<JSON.Obj>('{"a":3.14,"b":true,"c":[1,2,3],"d":{"x":1,"y":2,"z":3}}');
console.log("Keys: " + obj.keys().join(" ")); // a b c d
console.log(
"Values: " +
obj
.values()
.map<string>((v) => JSON.stringify(v))
.join(" "),
); // 3.14 true [1,2,3] {"x":1,"y":2,"z":3}
const y = obj.get("d")!.get<JSON.Obj>().get("y")!;
console.log('o1["d"]["y"] = ' + y.toString()); // o1["d"]["y"] = 2
```
**Working with dynamic types within a schema**
More often, objects will be completely statically typed except for one or two values.
In such cases, `JSON.Value` can be used to handle fields that may hold different types at runtime.
```typescript
@json
class DynamicObj {
id: i32 = 0;
name: string = "";
data!: JSON.Value; // Can hold any type of value
}
const obj = new DynamicObj();
obj.id = 1;
obj.name = "Example";
obj.data = JSON.parse<JSON.Value>('{"key":"value"}'); // Assigning an object
console.log(JSON.stringify(obj)); // {"id":1,"name":"Example","data":{"key":"value"}}
obj.data = JSON.Value.from<i32>(42); // Changing to an integer
console.log(JSON.stringify(obj)); // {"id":1,"name":"Example","data":42}
obj.data = JSON.Value.from("a string"); // Changing to a string
console.log(JSON.stringify(obj)); // {"id":1,"name":"Example","data":"a string"}
```
**Working with nullable primitives and dynamic data**
```ts
const box = JSON.Box.from<i32>(123);
const value = JSON.Value.from<JSON.Box<i32> | null>(box);
const reboxed = JSON.Box.fromValue<i32>(value); // Box<i32> | null
console.log(reboxed !== null ? reboxed!.toString() : "null");
// 123
const value = JSON.parse<JSON.Value>("123");
const boxed = JSON.Box.fromValue<i32>(value);
console.log(boxed !== null ? boxed!.toString() : "null");
// 123
```
### Using Raw JSON strings
Sometimes its necessary to simply copy a string instead of serializing it.
For example, the following data would typically be serialized as:
```typescript
const map = new Map<string, string>();
map.set("pos", '{"x":1.0,"y":2.0,"z":3.0}');
console.log(JSON.stringify(map));
// {"pos":"{\"x\":1.0,\"y\":2.0,\"z\":3.0}"}
// pos's value (Vec3) is contained within a string... ideally, it should be left alone
```
If, instead, one wanted to insert Raw JSON into an existing schema/data structure, they could make use of the JSON.Raw type to do so:
```typescript
const map = new Map<string, JSON.Raw>();
map.set("pos", new JSON.Raw('{"x":1.0,"y":2.0,"z":3.0}'));
console.log(JSON.stringify(map));
// {"pos":{"x":1.0,"y":2.0,"z":3.0}}
// Now its properly formatted JSON where pos's value is of type Vec3 not string!
```
### Working with enums
By default, enums with values other than `i32` arn't supported by AssemblyScript. However, you can use a workaround:
```typescript
namespace Foo {
export const bar = "a";
export const baz = "b";
export const gob = "c";
}
type Foo = string;
const serialized = JSON.stringify<Foo>(Foo.bar);
// "a"
```
### Using custom serializers or deserializers
This library supports custom serialization and deserialization methods, which can be defined using the `@serializer` and `@deserializer` decorators.
Here's an example of creating a custom data type called `Point` which serializes to `(x,y)`
```typescript
import { bytes } from "json-as/assembly/util";
@json
class Point {
x: f64 = 0.0;
y: f64 = 0.0;
constructor(x: f64, y: f64) {
this.x = x;
this.y = y;
}
@serializer
serializer(self: Point): string {
return `(${self.x},${self.y})`;
}
@deserializer
deserializer(data: string): Point {
const dataSize = bytes(data);
if (dataSize <= 2) throw new Error("Could not deserialize provided data as type Point");
const c = data.indexOf(",");
const x = data.slice(1, c);
const y = data.slice(c + 1, data.length - 1);
return new Point(f64.parse(x), f64.parse(y));
}
}
const obj = new Point(3.5, -9.2);
const serialized = JSON.stringify<Point>(obj);
const deserialized = JSON.parse<Point>(serialized);
console.log("Serialized " + serialized);
console.log("Deserialized " + JSON.stringify(deserialized));
```
The serializer function converts a `Point` instance into a string format `(x,y)`.
The deserializer function parses the string `(x,y)` back into a `Point` instance.
These functions are then wrapped before being consumed by the json-as library:
```typescript
@inline __SERIALIZE_CUSTOM(): void {
const data = this.serializer(this);
const dataSize = data.length << 1;
memory.copy(bs.offset, changetype<usize>(data), dataSize);
bs.offset += dataSize;
}
@inline __DESERIALIZE_CUSTOM(data: string): Point {
return this.deserializer(data);
}
```
This allows custom serialization while maintaining a generic interface for the library to access.
## Performance
The `json-as` library is engineered for **multi-GB/s processing speeds**, leveraging SIMD and SWAR optimizations along with highly efficient transformations. The charts below highlight key performance metrics such as build time, operations-per-second, and throughput.
You can **re-run the benchmarks** at any time by clicking the button below. After the workflow completes, refresh this page to view updated results.
[](https://github.com/JairusSW/json-as/actions/workflows/benchmark.yml)
### Comparison to JavaScript
The following charts compare JSON-AS (both SWAR and SIMD variants) against JavaScript's native `JSON` implementation. Benchmarks were conducted in a GitHub Actions environment. On modern hardware, you may see even higher throughput.
> Note: Benchmarks reflect the **latest version**. Older versions may show different performance.
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart01.svg" alt="Performance Chart 1">
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart02.svg" alt="Performance Chart 2">
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart03.png" alt="Performance Chart 3">
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart04.png" alt="Performance Chart 3">
> Note: I have focused on extensively optimizing serialization. I used to have deserialization be highly unsafe and extremely fast, but I've since doubled down on safety for deserialization which has negatively affected performance. I will be optimizing soon.
### Performance Tuning
Instead of using flags for setting options, `json-as` is configured by environmental variables.
Here's a short list:
**JSON_CACHE** (default: 0) - Enables caching costly strings based on hit frequency. May boost string serialization in excess of 22 GB/s.
**JSON_DEBUG** (default: 0) - Sets the debug level. May be within range `0-3`
**JSON_MODE** (default: SWAR) - Selects which mode should be used. Can be `NAIVE,SWAR,SIMD`. Note that `--enable simd` may be required.
**JSON_WRITE** (default: "") - Select a series of files to output after transform and optimization passes have completed for easy inspection. Usage: `JSON_WRITE=.path-to-file-a.ts,./path-to-file-b.ts`
### Running benchmarks locally
Benchmarks are run directly on top of v8 for more tailored control
1. Download JSVU off of npm
```bash
npm install jsvu -g
```
2. Modify your dotfiles to add `~/.jsvu/bin` to `PATH`
```bash
export PATH="${HOME}/.jsvu/bin:${PATH}"
```
3. Clone the repository
```bash
git clone https://github.com/JairusSW/json-as
```
4. Install dependencies
```bash
npm i
```
5. Run benchmarks for either AssemblyScript or JavaScript
```bash
./run-bench.as.sh
```
or
```bash
./run-bench.js.sh
```
## Debugging
`JSON_DEBUG=1` - Prints out generated code at compile-time
`JSON_DEBUG=2` - The above and prints keys/values as they are deserialized
`JSON_WRITE=path-to-file.ts` - Writes out generated code to `path-to-file.json.ts` for easy inspection
## License
This project is distributed under an open source license. You can view the full license using the following link: [License](./LICENSE)
## Contact
Please send all issues to [GitHub Issues](https://github.com/JairusSW/json-as/issues) and to converse, please send me an email at [me@jairus.dev](mailto:me@jairus.dev)
- **Email:** Send me inquiries, questions, or requests at [me@jairus.dev](mailto:me@jairus.dev)
- **GitHub:** Visit the official GitHub repository [Here](https://github.com/JairusSW/json-as)
- **Website:** Visit my official website at [jairus.dev](https://jairus.dev/)
- **Discord:** Contact me at [My Discord](https://discord.com/users/600700584038760448) or on the [AssemblyScript Discord Server](https://discord.gg/assemblyscript/)