json-as
Version:
The only JSON library you'll need for AssemblyScript. SIMD enabled
448 lines (324 loc) • 15 kB
Markdown
<h1 align="center"><pre> ╦╔═╗╔═╗╔╗╔ ╔═╗╔═╗
║╚═╗║ ║║║║══╠═╣╚═╗
╚╝╚═╝╚═╝╝╚╝ ╩ ╩╚═╝</pre></h1>
## 💾 Installation
```bash
npm install json-as
```
Add the `--transform` to your `asc` command (e.g. in package.json)
```bash
--transform json-as/transform
```
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";
class Vec3 {
x: f32 = 0.0;
y: f32 = 0.0;
z: f32 = 0.0;
}
class Player {
("first name")
firstName!: string;
lastName!: string;
lastActive!: i32[];
// Drop in a code block, function, or expression that evaluates to a boolean
((self: Player) => self.age < 18)
age!: i32;
()
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:
****
This decorator excludes a field from serialization entirely.
```typescript
class Example {
name!: string;
SSN!: string;
}
const obj = new Example();
obj.name = "Jairus";
obj.SSN = "123-45-6789";
console.log(JSON.stringify(obj)); // { "name": "Jairus" }
```
****
This decorator omits a field only if its value is null.
```typescript
class Example {
name!: string;
()
optionalField!: string | null;
}
const obj = new Example();
obj.name = "Jairus";
obj.optionalField = null;
console.log(JSON.stringify(obj)); // { "name": "Jairus" }
```
**((self: this) => condition)**
This decorator omits a field based on a custom predicate function.
```typescript
class Example {
name!: string;
((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
class Person {
name!: string;
age: i32 | null = null;
}
```
Instead, use `JSON.Box` to allow nullable primitives:
```typescript
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
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"}
```
### 🏗️ 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 `` and `` 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";
class Point {
x: f64 = 0.0;
y: f64 = 0.0;
constructor(x: f64, y: f64) {
this.x = x;
this.y = y;
}
serializer(self: Point): string {
return `(${self.x},${self.y})`;
}
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
__SERIALIZE_CUSTOM(): void {
const data = this.serializer(this);
const dataSize = data.length << 1;
memory.copy(bs.offset, changetype<usize>(data), dataSize);
bs.offset += dataSize;
}
__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 has been optimized to achieve near-gigabyte-per-second JSON processing speeds through SIMD acceleration and highly efficient transformations. Below are detailed statistics comparing performance metrics such as build time, operations-per-second, and throughput.
### 🔍 Comparison to JavaScript
These benchmarks compare this library to JavaScript's native `JSON.stringify` and `JSON.parse` functions.
**Table 1** - _AssemblyScript (LLVM)_
| Test Case | Size | Serialization (ops/s) | Deserialization (ops/s) | Serialization (MB/s) | Deserialization (MB/s) |
| --------------- | ---------- | --------------------- | ----------------------- | -------------------- | ---------------------- |
| Vector3 Object | 38 bytes | 26,611,226 ops/s | 32,160,804 ops/s | 1,357 MB/s | 1,348 MB/s |
| Alphabet String | 104 bytes | 16,916,886 ops/s | 18,390,804 ops/s | 1,759 MB/s | 1,986 MB/s |
| Small Object | 88 bytes | 24,242,424 ops/s | 12,307,692 ops/s | 2,133 MB/s | 1,083 MB/s |
| Medium Object | 494 bytes | 4,060,913 ops/s | 1,396,160 ops/s | 2,006 MB/s | 689.7 MB/s |
| Large Object | 3374 bytes | 479,616 ops/s | 132,802 ops/s | 2,074 MB/s | 448.0 MB/s |
**Table 2** - _JavaScript (V8)_
| Test Case | Size | Serialization (ops/s) | Deserialization (ops/s) | Serialization (MB/s) | Deserialization (MB/s) |
| --------------- | ---------- | --------------------- | ----------------------- | -------------------- | ---------------------- |
| Vector3 Object | 38 bytes | 8,791,209 ops/s | 5,369,12 ops/s | 357.4 MB/s | 204.3 MB/s |
| Alphabet String | 104 bytes | 12,830,228 ops/s | 12,140,296 ops/s | 1,334 MB/s | 1,311 MB/s |
| Small Object | 88 bytes | 8,376,963 ops/s | 4,968,944 ops/s | 737.1 MB/s | 437.2 MB/s |
| Medium Object | 494 bytes | 2,395,210 ops/s | 1,381,693 ops/s | 1,183 MB/s | 682.5 MB/s |
| Large Object | 3374 bytes | 222,222 ops/s | 117,233 ops/s | 749.7 MB/s | 395.5 MB/s |
**📌 Insights**
- JSON-AS consistently outperforms JavaScript's native implementation.
- **Serialization Speed:**
- JSON-AS achieves speeds up to `2,133 MB/s`, significantly faster than JavaScript's peak of `1,416 MB/s`.
- Large objects see the biggest improvement, with JSON-AS at `2,074 MB/s` vs. JavaScript’s `749.7 MB/s`.
- **Deserialization Speed:**
- JSON-AS reaches `1,986 MB/s`, while JavaScript caps at `1,592 MB/s`.
- Small and medium objects see the most significant performance boost overall.
### 📈 Comparison to v0.9.x version
**Table 1** - _v1.0.0_
| Test Case | Size | Serialization (ops/s) | Deserialization (ops/s) | Serialization (MB/s) | Deserialization (MB/s) |
| --------------- | ---------- | --------------------- | ----------------------- | -------------------- | ---------------------- |
| Vector3 Object | 38 bytes | 35,714,285 ops/s | 35,435,552 ops/s | 1,357 MB/s | 1,348 MB/s |
| Alphabet String | 104 bytes | 13,617,021 ops/s | 18,390,804 ops/s | 1,416 MB/s | 1,986 MB/s |
| Small Object | 88 bytes | 24,242,424 ops/s | 12,307,692 ops/s | 2,133 MB/s | 1,083 MB/s |
| Medium Object | 494 bytes | 4,060,913 ops/s | 1,396,160 ops/s | 2,006 MB/s | 689.7 MB/s |
| Large Object | 3374 bytes | 614,754 ops/s | 132,802 ops/s | 2,074 MB/s | 448.0 MB/s |
**Table 2** - _v0.9.29_
| Test Case | Size | Serialization (ops/s) | Deserialization (ops/s) | Serialization (MB/s) | Deserialization (MB/s) |
| --------------- | ---------- | --------------------- | ----------------------- | -------------------- | ---------------------- |
| Vector3 Object | 38 bytes | 6,896,551 ops/s | 10,958,904 ops/s | 262.1 MB/s | 416.4 MB/s |
| Alphabet String | 104 bytes | 5,128,205 ops/s | 8,695,652 ops/s | 533.3 MB/s | 939.1 MB/s |
| Small Object | 88 bytes | 4,953,560 ops/s | 3,678,160 ops/s | 435.9 MB/s | 323.7 MB/s |
| Medium Object | 494 bytes | 522,193 ops/s | 508,582 ops/s | 258.0 MB/s | 251.2 MB/s |
| Large Object | 3374 bytes | 51,229 ops/s | 65,585 ops/s | 172.8 MB/s | 221.3 MB/s |
**📌 Insights:**
- Massive performance improvements in JSON-AS `v1.0.0`:
- Serialization is **2-12x faster** (e.g., Large Object: `2,074 MB/s` vs. `172.8 MB/s`).
- Deserialization is **2-3x faster** (e.g., Large Object: `1,348 MB/s` vs. `221.3 MB/s`).
- Vector3 Object serialization improved from `416 MB/s` to `1,357 MB/s`--a **3x benefit** through new code generation techniques.
## 🔭 What's Next
- Theorize plans to keep key-order in generated schemas
- Generate optimized deserialization methods
- Inline specific hot code paths
- Implement error handling implementation
## 🐛 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.dev](mailto:me.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/)