◆ 同じ構造のデータの配列の場合 キー名の繰り返しがムダ
  ◆ ファイルサイズが増える原因
◆ (1) JSON のまま無駄をなくす
  ◆ キーを 1, 2 文字に変換してマッピング情報と変換後の JSON をまとめる
◆ (2) CSV 風に書けるフォーマットを作る

JSON のキー

JSON は見やすく便利なのですがキー名が冗長です
オブジェクトの配列の場合 配列の要素全部に対して同じキーを書くことになります

[
{ "id": 1, "name": "a", "count": 10 },
{ "id": 2, "name": "r", "count": 30 },
{ "id": 3, "name": "v", "count": 20 },
]

SQL の結果を JSON にすると考えるとイメージが付きやすいと思います

キーがなんなのかをわかりやすくするため 数単語を組み合わせた長い名前にすると特にその影響が大きいです
値が 1, 2 桁程度の数値だとしたら ほとんどがキー名です
JSON 文字列中の半分以上がキー名ということも少なくないと思います
さらに配列の要素数が増えるほど重複したムダな部分も増えます

データ量が大したこと無いならあまり気にもしませんが 数千件くらいになってくると重くなってきてデータ量を減らしたくなります
本当に SQL の結果であれば 2 次元配列と列名の配列で済むのですが データ構造にネストしたオブジェクトがあったりするかもしれません

圧縮したい

これをいい感じで解決したいなーと思って代替のフォーマットを考えることにしました

JSON を使う

単純にやるなら JSON 中のキー名をスキャンして 3 文字以上は 2 文字以内の文字列に置換します

[
{ "longlongname1": 10, "longlongname2": [{ "longname1": 3 }] }
{ "longlongname1": 11, "longlongname2": [{ "longname1": 4 }] }
]

だったら "longlongname1" を "a" にして "longlongname2" を "b" にして "longname1" を "c" にします
そしてこの変換情報をオブジェクトにします

{ "a": "longlongname1", "b": "longlongname2", "c": "longname1" }

置換済みの JSON とマッピングのオブジェクトをまとめます

[
{ "a": "longlongname1", "b": "longlongname2", "c": "longname1" },
[
{ "a": 10, "b": [{ "c": 3 }] },
{ "a": 11, "b": [{ "c": 4 }] }
]
]

これで長いキー名は最初の一回で あとは 1 文字で済むようになりました
オブジェクトのキー名の種類が多い場合は 1 文字だと表せずに 2 文字になるかもしれません
大文字小文字数字記号を合わせれば 70 文字くらいはあるので 2 文字あれば 70*70=4900 種類です
さすがに 3 文字はいらないでしょう

復元はこういう感じです

const [map, value] = data

const restore = value => {
if (Array.isArray(value)) {
return value.map(rec)
} else if (value instanceof Object) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => {
return [
map[k] || k,
rec(v)
]
})
)
}
return value
}

const result = restore(value)
console.log(result)

これでも十分なのですが JSON である以上キー名はすべて 「""」 が付きますし 値も文字列なら 「""」 が付きますし boolean 値なら true か false です
もっと情報削れるなーと思って JSON を使わないものも作ってみることにしました

JSON を使わない

ターゲットにするのは同じ型の繰り返しの配列です
なので構造は共通という前提です
同じものならそれぞれのデータに書く必要はありません

ヘッダとして型定義を書いて 以降はデータの中身だけを書けるようにしようと思います
イメージは CSV です
ただし ネストするオブジェクトや配列があるので そのまま CSV にはできません

型定義には構造だけでなく number や string も指定することで値を短く書けるようにします
型がわかるならわざわざ 「""」 で囲む必要もないです
string 型の 「5」 を 「"5"」 にしなくても 「5」 のままで十分です
boolean も true/false と書かなくても t/f だけで十分です

シンプルなケースはこう書けるようにします

b
t
f
f
[true, false, false]

1 行目の b がデータは boolean 型だけということを表します
2 行目以降のデータの t か f を boolean 型としてパースします

number 型は n で string 型は s です

n
10
3.1
-30
[10, 3.1, -30]

s
a
xyz
foo
bar
["a", "xyz", "foo", "bar"]

肝心のオブジェクト型ですが 「{prop1:n, prop2:s}」 のような書き方にしました
JSON に似ています
「,」 区切りでプロパティを定義し それぞれ 「name:type」 のフォーマットです
name はオブジェクトのプロパティ名そのままで type は上で書いた型で n や s です

値は 「,」 区切りでヘッダの定義順で書きます
ヘッダ部分は見た目がほぼ JSON なので JSON でもよかったのですが JSON でオブジェクトをパースした場合 元の順番が保証されないのでやめました
どうせ自力でパースするなら JSON より短くかけるようにしています

{a:n,b:n,c:n}
1,2,3
3,4,1
[{a:1, b:2, c:3}, {a:3, b:4, c:1}]

型定義のところに n や s ではなく {} を書けばネストしたオブジェクトも対応しています
いくらネストしても値のほうでは {} は要らないフラットなものなのでテキスト量が少なくなります

配列の場合は [] を使います
[] の中に型定義を一つ入れます
そうすると その型の任意個数の配列になります

2 つ以上入れるとタプル扱いです
JavaScript 上は同じ配列ですが要素数が固定になりそれぞれ型定義に指定した型になります
「[n,s]」 は 0 番目が number で 1 番目が string の [1, "a"] のようなデータの型定義です

配列の場合は要素数が可変なので 値の方でも [] を使います
タプルの場合は要素数固定なのでなくてもいいのですが 配列に合わせて [] を入れてます

{a:n,b:n,c:[n,s],d:{x:{y:{z:b}}},e:[n],f:s}
1,2,[1,djea],f,[1,2,3,4],xxac
[{a:1, b:2, c:[1,"djea"], d:{x:{y:{z:false}}}, e:[1,2,3,4], f: "xxac"}]

またパースを楽にするため一部の文字はエスケープが必要です

#   -> ##
, -> #c
\n -> #r
: -> #C
" " -> #s

tcsv

この JSON を使わない方法のファイルは独自フォーマットなので名前があったのほうがいいので tcsv にしました
見た目 「,」 区切りなので CSV ですが 本来の CSV とは違うので型指定してるところから typed の t をつけました

コード

長くなったので Gist に置いてます

上の Gist の example.js にもあるのですが使用例です
JavaScript 中のオブジェクトを serialize して deserialize します

import { serialize, deserialize } from "./tcsv.js"

const def = "{a:n,b:n,c:[n,s],d:{x:{y:{z:b}}},e:s,f:[[n]],g:[[s,s]]}"
const data = [
{
a: 10,
b: 2,
c: [1, "J"],
d: {
x: {
y: {
z: false,
},
},
},
e: "fff",
f: [[1, 2, 3], [3, 4, 5]],
g: [["a", "b"], ["c", "d"], ["e", "g"], ["g", "h"]],
},
{
a: 123,
b: "7",
c: [1, 2],
d: {
x: {
y: {
z: 1,
},
},
},
e: null,
f: [[11, "22", 33], [33, "44", 55]],
g: [[4, 5]],
},
]

const tcsv_str = serialize(data, def)
console.log(tcsv_str)
/*
{a:n,b:n,c:[n,s],d:{x:{y:{z:b}}},e:s,f:[[n]],g:[[s,s]]}
10,2,[1,J],f,fff,[[1,2,3],[3,4,5]],[[a,b],[c,d],[e,g],[g,h]]
123,7,[1,2],t,#N,[[11,22,33],[33,44,55]],[[4,5]]
*/

const org = deserialize(tcsv_str)
console.log(JSON.stringify(org.body, null, " "))
/*
[
{
"a": 10,
"b": 2,
"c": [1, "J"],
"d": {
"x": {
"y": {
"z": false
}
}
},
"e": "fff",
"f": [
[1, 2, 3],
[3, 4, 5]
],
"g": [
["a", "b"],
["c", "d"],
["e", "g"],
["g", "h"]
]
},
{
"a": 123,
"b": 7,
"c": [1, "2"],
"d": {
"x": {
"y": {
"z": true
}
}
},
"e": null,
"f": [
[11, 22, 33],
[33, 44, 55]
],
"g": [
["4", "5"]
]
}
]
*/

型を指定しているので 実際の値が別の型でも指定の型に変換されます

制限

型定義のテキストを自動生成する仕組みは用意してないので 毎回手書きが必要です

また 思いつきととりあえずで作ったので完全に JSON データを表現できません
一応配列やオブジェクトのミックスには対応してますが 型定義に複合型がありません

数値かもしれないし文字列かもしれない みたいなデータは扱えません
プロパティの単体の値ならあまり困らないかもしれませんが 配列の場合に困るかもしれません
タプルとして扱う要素数固定なら ◯番目の型を書いていけばいいですが任意個数だと型は一つだけです
配列の要素の型が全部一緒じゃないといけないのは扱いづらいことがありそうです

CSV のように考えて any 型は string に置き換えて deserialize 後にそこだけ手動で変換すれば一応復元できなくはないです

オブジェクト定義の複合型は 各オブジェクトのプロパティをマージした型定義にすれば 競合するプロパティ名がなければ対応できます
SQL の LEFT JOIN で右側が見つからず NULL で埋まってるみたいな感じなので多少ムダがありますけど

また 値は 「,」 区切りでプリミティブ値を列挙するという性質上 オブジェクトの部分で null を指定することができません
絶対にオブジェクトを作った上で プロパティのプリミティブ値が null になります