Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# JSON-AS

JSON-AS maps JSON documents to strongly typed AssemblyScript values, with schema-derived behavior optimized for predictable execution.

## Language

**JSON Struct**:
An AssemblyScript class whose JSON shape is declared with `@json` and can be serialized or deserialized according to that shape.
_Avoid_: JSON model, decorated object

**Generated Codec**:
The schema-derived serialization and deserialization behavior associated with a JSON Struct.
_Avoid_: generated serde, generated parser

**Canonical Input**:
A compact JSON object whose keys appear in the JSON Struct's declared schema order, with no insignificant whitespace.
_Avoid_: happy path, normal JSON

**Lazy Field**:
A JSON Struct field whose value is represented by its source range until first access.
_Avoid_: deferred property, lazy slot

**Fresh Deserialization**:
Deserialization that creates a new JSON Struct.
_Avoid_: allocating parse

**Reuse Deserialization**:
Deserialization that writes into a caller-provided JSON Struct while retaining reusable allocations where possible.
_Avoid_: in-place parse, cached parse
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,15 @@ Here's a short list:

**JSON_MODE** (default: SWAR) - Selects which mode should be used. Can be `NAIVE,SWAR,SIMD`. Note that `--enable simd` may be required.

**JSON_SIMD_WIDTH** (default: 128) - Selects 128-, 256-, or 512-bit string scans in SIMD mode. The 256/512 paths use [`as-simd`](https://github.com/JairusSW/as-simd) fused memory predicates, while general wide operations use the portable externref ABI consumed by Wago's [`JairusSW/wide`](https://github.com/JairusSW/wide) plugin:

```bash
WAGO_PLUGINS=wide JSON_MODE=SIMD JSON_SIMD_WIDTH=512 \
asc app.ts --transform json-as --transform as-simd --enable simd
```

Run `npm run test:wago-wide` to compile and execute both native-width variants against current Wago and Wide. `JSON_SIMD_WIDTH=256/512` without `--enable simd` is rejected at compile time.

**JSON_USE_FAST_PATH** (default: 1) - The transform emits the fast `__DESERIALIZE` implementation for generated structs by default. Set to `0`, `false`, `off`, or `no` to force slow-path-only output. See [FAST_PATH_DESERIALIZE.md](./FAST_PATH_DESERIALIZE.md) for the current support matrix, known gaps, and dedicated test command.

**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`
Expand Down
391 changes: 391 additions & 0 deletions SIMDJSON_NOTES.md

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion assembly/__benches__/classic/canada.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect } from "../../__tests__/lib";
import {
blackbox,
bench,
ChangingPayloads,
dumpToFile,
readFile,
utf8ByteLength,
Expand Down Expand Up @@ -40,6 +41,9 @@ const prettyJson = readFile(
"./assembly/__benches__/payloads/canada.pretty.json",
);
const minJson = readFile("./assembly/__benches__/payloads/canada.min.json");
const freshPayloads = new ChangingPayloads(minJson);
const reusePayloads = new ChangingPayloads(minJson);
const reuseTarget = JSON.parse<Canada>(minJson);

expect(JSON.stringify(JSON.parse<Canada>(prettyJson))).toBe(minJson);
expect(JSON.stringify(JSON.parse<Canada>(minJson))).toBe(minJson);
Expand All @@ -60,12 +64,21 @@ dumpToFile("canada-pretty", "deserialize");
bench(
"Deserialize Canada (min)",
() => {
blackbox(JSON.parse<Canada>(minJson));
blackbox(JSON.parse<Canada>(freshPayloads.next()));
},
500,
utf8ByteLength(minJson),
);
dumpToFile("canada-min", "deserialize");
bench(
"Deserialize Canada (min, reuse)",
() => {
blackbox(JSON.parse<Canada>(reusePayloads.next(), reuseTarget));
},
500,
utf8ByteLength(minJson),
);
dumpToFile("canada-min-reuse", "deserialize");

bench(
"Serialize Canada (min)",
Expand Down
15 changes: 14 additions & 1 deletion assembly/__benches__/classic/github_events.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect } from "../../__tests__/lib";
import {
blackbox,
bench,
ChangingPayloads,
dumpToFile,
readFile,
utf8ByteLength,
Expand Down Expand Up @@ -245,6 +246,9 @@ const prettyJson = readFile(
const minJson = readFile(
"./assembly/__benches__/payloads/github_events.min.json",
);
const freshPayloads = new ChangingPayloads(minJson);
const reusePayloads = new ChangingPayloads(minJson);
const reuseTarget = JSON.parse<GhEvent[]>(minJson);

expect(JSON.parse<GhEvent[]>(minJson).length).toBe(30);

Expand All @@ -264,12 +268,21 @@ dumpToFile("github_events-pretty", "deserialize");
bench(
"Deserialize GitHubEvents (min)",
() => {
blackbox(JSON.parse<GhEvent[]>(minJson));
blackbox(JSON.parse<GhEvent[]>(freshPayloads.next()));
},
20000,
utf8ByteLength(minJson),
);
dumpToFile("github_events-min", "deserialize");
bench(
"Deserialize GitHubEvents (min, reuse)",
() => {
blackbox(JSON.parse<GhEvent[]>(reusePayloads.next(), reuseTarget));
},
20000,
utf8ByteLength(minJson),
);
dumpToFile("github_events-min-reuse", "deserialize");

bench(
"Serialize GitHubEvents (min)",
Expand Down
15 changes: 14 additions & 1 deletion assembly/__benches__/classic/twitter.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect } from "../../__tests__/lib";
import {
blackbox,
bench,
ChangingPayloads,
dumpToFile,
readFile,
utf8ByteLength,
Expand Down Expand Up @@ -246,6 +247,9 @@ const prettyJson = readFile(
"./assembly/__benches__/payloads/twitter.pretty.json",
);
const minJson = readFile("./assembly/__benches__/payloads/twitter.min.json");
const freshPayloads = new ChangingPayloads(minJson);
const reusePayloads = new ChangingPayloads(minJson);
const reuseTarget = JSON.parse<Twitter>(minJson);
const outStr = "";

expect(JSON.parse<Twitter>(minJson).statuses.length).toBe(100);
Expand All @@ -265,12 +269,21 @@ dumpToFile("twitter-pretty", "deserialize");
bench(
"Deserialize Twitter (min)",
() => {
blackbox(JSON.parse<Twitter>(minJson));
blackbox(JSON.parse<Twitter>(freshPayloads.next()));
},
2000,
utf8ByteLength(minJson),
);
dumpToFile("twitter-min", "deserialize");
bench(
"Deserialize Twitter (min, reuse)",
() => {
blackbox(JSON.parse<Twitter>(reusePayloads.next(), reuseTarget));
},
2000,
utf8ByteLength(minJson),
);
dumpToFile("twitter-min-reuse", "deserialize");

bench(
"Serialize Twitter (min)",
Expand Down
22 changes: 20 additions & 2 deletions assembly/__benches__/large.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { JSON } from "..";
import { expect } from "../__tests__/lib";
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
import {
bench,
blackbox,
ChangingPayloads,
dumpToFile,
utf8ByteLength,
} from "./lib/bench";


@json
Expand Down Expand Up @@ -122,6 +128,9 @@ class Repo {
// Create instances and assign fields directly
const v2 = `{"id":132935648,"node_id":"MDEwOlJlcG9zaXRvcnkxMzI5MzU2NDg=","name":"boysenberry-repo-1","full_name":"octocat/boysenberry-repo-1","private":true,"owner":{"login":"octocat","id":583231,"node_id":"MDQ6VXNlcjU4MzIzMQ==","avatar_url":"https://avatars.githubusercontent.com/u/583231?v=4","gravatar_id":"","url":"https://api.github.com/users/octocat","html_url":"https://github.com/octocat","followers_url":"https://api.github.com/users/octocat/followers","following_url":"https://api.github.com/users/octocat/following{/other_user}","gists_url":"https://api.github.com/users/octocat/gists{/gist_id}","starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/octocat/subscriptions","organizations_url":"https://api.github.com/users/octocat/orgs","repos_url":"https://api.github.com/users/octocat/repos","events_url":"https://api.github.com/users/octocat/events{/privacy}","received_events_url":"https://api.github.com/users/octocat/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/octocat/boysenberry-repo-1","description":"Testing","fork":true,"url":"https://api.github.com/repos/octocat/boysenberry-repo-1","forks_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/forks","keys_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/keys{/key_id}","collaborators_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/teams","hooks_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/hooks","issue_events_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues/events{/number}","events_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/events","assignees_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/assignees{/user}","branches_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/branches{/branch}","tags_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/tags","blobs_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/refs{/sha}","trees_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/trees{/sha}","statuses_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/statuses/{sha}","languages_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/languages","stargazers_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/stargazers","contributors_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/contributors","subscribers_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/subscribers","subscription_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/subscription","commits_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/commits{/sha}","git_commits_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/commits{/sha}","comments_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/comments{/number}","issue_comment_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues/comments{/number}","contents_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/contents/{+path}","compare_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/compare/{base}...{head}","merges_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/merges","archive_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/downloads","issues_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues{/number}","pulls_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/pulls{/number}","milestones_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/milestones{/number}","notifications_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/labels{/name}","releases_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/releases{/id}","deployments_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/deployments","created_at":"2018-05-10T17:51:29Z","updated_at":"2025-05-24T02:01:19Z","pushed_at":"2024-05-26T07:02:05Z","git_url":"git://github.com/octocat/boysenberry-repo-1.git","ssh_url":"git@github.com:octocat/boysenberry-repo-1.git","clone_url":"https://github.com/octocat/boysenberry-repo-1.git","svn_url":"https://github.com/octocat/boysenberry-repo-1","homepage":"","size":4,"stargazers_count":332,"watchers_count":332,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":20,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":20,"open_issues":1,"watchers":332,"default_branch":"master"}`;
const v1 = JSON.parse<Repo>(v2);
const freshPayloads = new ChangingPayloads(v2);
const reusePayloads = new ChangingPayloads(v2);
const reuseTarget = JSON.parse<Repo>(v2);
const byteLength: usize = utf8ByteLength(v2);
expect(JSON.stringify(JSON.parse<Repo>(v2))).toBe(v2);
bench(
Expand All @@ -136,12 +145,21 @@ dumpToFile("large", "serialize");
bench(
"Deserialize Large API Response",
() => {
blackbox(JSON.parse<Repo>(v2));
blackbox(JSON.parse<Repo>(freshPayloads.next()));
},
10_000,
byteLength,
);
dumpToFile("large", "deserialize");
bench(
"Deserialize Large API Response (reuse)",
() => {
blackbox(JSON.parse<Repo>(reusePayloads.next(), reuseTarget));
},
10_000,
byteLength,
);
dumpToFile("large-reuse", "deserialize");

// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
const objLarge = JSON.parse<JSON.Obj>(v2);
Expand Down
22 changes: 20 additions & 2 deletions assembly/__benches__/medium.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { JSON } from "..";
import { expect } from "../__tests__/lib";
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
import {
bench,
blackbox,
ChangingPayloads,
dumpToFile,
utf8ByteLength,
} from "./lib/bench";


@json
Expand Down Expand Up @@ -44,6 +50,9 @@ class MediumAPIResponse {

const v2 = `{"id":42,"username":"jairus","full_name":"Jairus Tanaka","email":"me@jairus.dev","avatar_url":"https://avatars.githubusercontent.com/u/123456?v=4","bio":"I like compilers, elegant algorithms, bare metal, simd, and wasm.","website":"https://jairus.dev/","location":"Seattle, WA","joined_at":"2020-01-15T08:30:00Z","is_verified":true,"is_premium":true,"follower_count":61,"following_count":39,"preferences":{"theme":"dark","notifications":true,"language":"en-US","timezone":"America/Los_Angeles","privacy_level":"friends_only","two_factor_enabled":false},"tags":["typescript","webassembly","performance","rust","assemblyscript","json"],"recent_activity":[{"action":"starred","timestamp":"2025-12-22T10:15:00Z","target":"assemblyscript/json-as"},{"action":"commented","timestamp":"2025-12-22T09:42:00Z","target":"issue #142"},{"action":"pushed","timestamp":"2025-12-21T23:58:00Z","target":"main branch"},{"action":"forked","timestamp":"2025-12-21T18:20:00Z","target":"fast-json-wasm"},{"action":"created","timestamp":"2025-12-21T14:10:00Z","target":"new benchmark suite"}]}`;
const v1 = JSON.parse<MediumAPIResponse>(v2);
const freshPayloads = new ChangingPayloads(v2);
const reusePayloads = new ChangingPayloads(v2);
const reuseTarget = JSON.parse<MediumAPIResponse>(v2);
const byteLength: usize = utf8ByteLength(v2);
expect(JSON.stringify(JSON.parse<MediumAPIResponse>(v2))).toBe(v2);
bench(
Expand All @@ -58,12 +67,21 @@ dumpToFile("medium", "serialize");
bench(
"Deserialize Medium API Response",
() => {
blackbox(JSON.parse<MediumAPIResponse>(v2));
blackbox(JSON.parse<MediumAPIResponse>(freshPayloads.next()));
},
500_000,
byteLength,
);
dumpToFile("medium", "deserialize");
bench(
"Deserialize Medium API Response (reuse)",
() => {
blackbox(JSON.parse<MediumAPIResponse>(reusePayloads.next(), reuseTarget));
},
500_000,
byteLength,
);
dumpToFile("medium-reuse", "deserialize");

// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
const objMedium = JSON.parse<JSON.Obj>(v2);
Expand Down
22 changes: 20 additions & 2 deletions assembly/__benches__/small.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { JSON } from "..";
import { expect } from "../__tests__/lib";
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
import {
bench,
blackbox,
ChangingPayloads,
dumpToFile,
utf8ByteLength,
} from "./lib/bench";


@json
Expand All @@ -11,6 +17,9 @@ class SmallJSON {
}
const v2 = `{"id":1,"name":"Small Object","active":true}`;
const v1 = JSON.parse<SmallJSON>(v2);
const freshPayloads = new ChangingPayloads(v2);
const reusePayloads = new ChangingPayloads(v2);
const reuseTarget = JSON.parse<SmallJSON>(v2);
const byteLength: usize = utf8ByteLength(v2);
expect(JSON.stringify(JSON.parse<SmallJSON>(v2))).toBe(v2);
bench(
Expand All @@ -25,12 +34,21 @@ dumpToFile("small", "serialize");
bench(
"Deserialize Small Object",
() => {
blackbox(JSON.parse<SmallJSON>(v2));
blackbox(JSON.parse<SmallJSON>(freshPayloads.next()));
},
5_000_000,
byteLength,
);
dumpToFile("small", "deserialize");
bench(
"Deserialize Small Object (reuse)",
() => {
blackbox(JSON.parse<SmallJSON>(reusePayloads.next(), reuseTarget));
},
5_000_000,
byteLength,
);
dumpToFile("small-reuse", "deserialize");

// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
const objSmall = JSON.parse<JSON.Obj>(v2);
Expand Down
Loading
Loading