世界を動かす技術を、日本語で。

Sj.h: 約150行のC99で書かれた小さなJSONパースライブラリ

概要

sj.h は、C99で書かれた約150行の 超小型JSONパーサーゼロアロケーション 設計で、状態管理も最小限。 エラー時は 行:列 情報付きのメッセージを出力。 数値や文字列のパースは ライブラリ外部で処理 する方針。 パブリックドメイン として公開され、商用利用も自由。

sj.h 超小型JSONパーサーの特徴

  • C99準拠 かつ約150行のシンプル実装
  • 動的メモリアロケーション非依存 設計
  • 最小限の内部状態管理 による高速動作
  • エラー発生時は行番号・列番号を含むメッセージ を返却
  • 数値パース機能非搭載
    • 利用者が strtodatoi などで独自処理
  • 文字列パース機能非搭載
    • Unicodeサロゲートペア などのハンドリングは利用者側で実装
  • パブリックドメインライセンス
    • 商用・非商用問わず自由に利用可能

基本的な使い方

  • JSON文字列から構造体への変換例
    • JSON例: { "x": 10, "y": 20, "w": 30, "h": 40 }
    • 変換先構造体: Rect(int型のx, y, w, hフィールドを持つ)
char *json_text = "{ \"x\": 10, \"y\": 20, \"w\": 30, \"h\": 40 }";
typedef struct { int x, y, w, h; } Rect;

bool eq(sj_Value val, char *s) {
  size_t len = val.end - val.start;
  return strlen(s) == len && !memcmp(s, val.start, len);
}

int main(void) {
  Rect rect = {0};
  sj_Reader r = sj_reader(json_text, strlen(json_text));
  sj_Value obj = sj_read(&r);
  sj_Value key, val;
  while (sj_iter_object(&r, obj, &key, &val)) {
    if (eq(key, "x")) { rect.x = atoi(val.start); }
    if (eq(key, "y")) { rect.y = atoi(val.start); }
    if (eq(key, "w")) { rect.w = atoi(val.start); }
    if (eq(key, "h")) { rect.h = atoi(val.start); }
  }
  printf("rect: { %d, %d, %d, %d }\n", rect.x, rect.y, rect.w, rect.h);
  return 0;
}
  • オブジェクトイテレーション
    • sj_iter_object でkey-valueペアを繰り返し取得
  • 値の比較
    • eq関数 でJSONのキー名と比較
  • 値の変換
    • atoi など標準関数で数値へ変換

注意点・制約

  • 数値パースは外部関数任せ
    • 浮動小数点や整数のフォーマットエラーは 自己責任
  • 文字列エスケープやUnicode処理は未対応
    • 必要に応じて 独自実装 が必要
  • JSON仕様の一部(例:特殊なUnicodeやエスケープ文字)には非対応
  • エラーメッセージ行:列 情報付きで分かりやすい
  • スレッドセーフ設計ではない ため、必要に応じて保護処理を実装

ライセンス・利用条件

  • パブリックドメイン
    • 商用・非商用問わず制限なし
    • クレジット表記不要
  • 詳細はLICENSEファイル 参照

関連資料

  • demoフォルダ に追加サンプルコード
  • 公式リポジトリ で最新情報・アップデートを確認

sj.h は、 組み込み用途最小限のJSONパースが必要なCプロジェクト に最適な選択肢。 自己責任での拡張カスタマイズ が前提のため、柔軟な運用が可能。

Hackerたちの意見

これは面白いけど、適合テストはどうなの? https://github.com/nst/JSONTestSuite

ほんとに聞きたいんだけど、ネストされたオブジェクトは扱えるの?

実際の特定の実装に基づいた適合テストがあったらめっちゃ役立つと思うんだ。具体的には、特定のターゲットパーサーライブラリのパース動作に完全に一致するサブセット(またはスーパーセット?)があればいいなって。これがなぜ役立つかっていうと、同じJSONが異なるパーサーによって異なって扱われることに依存する脆弱性を避けるためなんだ(例えば、これを利用して認可レイヤーを回避できる)。

検証があまりないみたいで、例えばオブジェクトや配列を終了させるのに ']' や '}' を無差別に使わせるんだよね。それに、'\v' をホワイトスペースとして許可する点では、RFCやjson.orgのJSONよりも寛容だと思う。もっと「正しいJSONからデータを抽出するツール」として扱った方がいいかも。でもそれでも、自分で文字列や数値のパーサーを作るのは面倒になるかもしれない。プロデューサーがJSON構文のサブセットに合意しない限りはね。

こういうのって何に使うの? JSON用の優れたライブラリはたくさんあるし。これは教育用のツールなの?

ゼロアロケーションで最小限の状態

コードが小さいとレビューしやすいから、厳しいセキュリティ要件のあるプロジェクトには向いてるかもね。それに、ライセンスの遵守も簡単(通知不要だし)。

これは特定の用途に合わせて改造するためのミニマルなライブラリを意図してるんじゃないかな。

埋め込みCPUは簡単だね。今や、ベイプの上でAPIサーバーを動かせるかも。

既存のコードベースに統合するのは超簡単だし、サイズのオーバーヘッドも最小限。ヒープの割り当てもないし、標準ライブラリも使ってない(型定義のためにstdbool.hとstddef.hだけ含まれてる)。C++のテンプレートのごちゃごちゃもないし、APIもすごくシンプルでわかりやすい。これらの条件を満たすCライブラリは実際にはかなり珍しいし、C++ライブラリはもっと珍しいよね。

メモリを割り当てない小さな単一ファイルの純粋なC依存関係は、うまく動けば共通の問題に対する普遍的な解決策になり得る。

オーバーヘッドが少なく、割り当てもなしでパースできるのは結構面白いよね。例えば、大量のJSONダンプを処理して特定のプロパティを抽出する時(Wikidataのダンプが思い浮かぶ)。

初心者向けの基本的なリファレンスとか、簡単なパースをしたい人向けかな?小さな趣味プロジェクトで、限られたプロセッサー用に小さなコードフットプリントが欲しい人向け?でもその場合、ほぼ確実にTOMLとか似たようなものを使うと思うけど。

ArduinoではKBしかないからね、GBやMBじゃなくて。

すべてにユースケースが必要ってわけじゃないし、ただのクールなプロジェクトだよ。

この作者の作品が好きなのは、たいてい単一ファイルのライブラリで、ANSI CやLuaで書かれていて、焦点が絞られてて使いやすいインターフェースと良いドキュメントがあるところ。しかもフリーソフトウェアライセンスだし。投稿されたプロジェクト以外で好きなのは:

  • log.c - C99で実装されたシンプルなロギングライブラリ
  • microui - 小さな即時モードのUIライブラリ
  • fe - ANSI Cで実装された小さく埋め込める言語
  • microtar - ANSI Cで書かれた軽量なtarライブラリ
  • cembed - Cヘッダーにファイルを埋め込むための小さなユーティリティ
  • ini - .ini設定ファイルを読み込むための小さなANSI Cライブラリ
  • json.lua - Lua用の軽量JSONライブラリ
  • lite - Luaで書かれた軽量テキストエディタ
  • cmixer - ゲーム用のポータブルANSI Cオーディオミキサー
  • uuid4 - uuid4文字列を生成するための小さなCライブラリ

オープンソースだけど、フリーソフトウェアではないよ。

ああ、そういえば、LOVE2Dでゲーム作ってた時に彼らのLumeライブラリを使ったことがある。IRCチャットで何回か会ったこともあって(彼らのアイデアの一つが悪いって言っちゃったけど、ごめんねrxi、調べたら実際にはいいアイデアだったわ笑) https://github.com/rxi/lume

Cプロジェクトではいつもlog.cを使ってるよ!著者が結構な数を書いてるなんて知らなかった。log.cをチェックするのをおすすめするよ。必要なものをハックするのがすごく簡単だから。

JSONパーサーライブラリって、ほんとに苦痛のブラックホールだと思う。使い方が全然違ったり、複雑な抽象のごちゃごちゃだったりすることが多いし、両方のケースもある。特定のユースケースに必要なものだけをちゃんと書けば、そんなに難しい問題じゃないのにね。

このライブラリは、これ以上「意見なし」ってのはないかも。キーや配列のアイテムを反復処理して、値のタイプを特定して、文字列スライスを返すだけ。

このプロジェクトは、最小限の状態でゼロアロケーションを謳ってるけど、正直言ってそれは公平じゃないし、私たちの問題は全然違うと思う。シングルストリング(最も使われるタイプ)だと、アロケーションが必要なんだよね。

現代のJSONライブラリがどれだけ複雑になるか、驚くべきことだよね。かつて「とてもシンプル」だったnlohmannのC++シングルヘッダーJSONライブラリは、今や * 13年経って * まだPRを積極的にマージしてる(最後のは5時間前) * 122 百万 のユニットテストがあるんだ。それでも、自分で言ってる通り、C++でJSONをパースする最速の方法ではないんだよね。そういうのを求めるなら、simdjsonを調べてみて。自分のJSONパーサーライブラリを作るのはやめた方がいいよ。絶対に。90%は45分でホワイトボードに書けるけど、残りの10%は1万時間かかるから。

JSONをパースするのは地雷原だよ (2016) https://seriot.ch/projects/parsing_json.html

「そんなに難しい問題じゃない」って言う人は、実際にその問題を解いたことがないんだよね。

一般的にJSONパーサーライブラリは苦しみのブラックホールだと思う。ここにいるSexprs、愛を求めてるよ。

このライブラリは、ここで符号付き整数のオーバーフローをチェックしてないよね。https://github.com/rxi/sj.h/blob/eb725e0858877e86932128836c1... https://github.com/rxi/sj.h/blob/eb725e0858877e86932128836c1... https://github.com/rxi/sj.h/blob/eb725e0858877e86932128836c1... 特定の入力によっては未定義動作を引き起こす可能性がある。

PRを提出してみて!

膨れ上がったエッジケースライブラリについてのいい記事があったよ [0](議論 [1])。時には、ライブラリの責任じゃないこともある。すべての可能なエラーを処理しようとすると、すぐに複雑になっちゃうからね。[0]: https://43081j.com/2025/09/bloat-of-edge-case-libraries [1]: https://news.ycombinator.com/item?id=45319399

非古代プラットフォームでは、intは32ビットだから、これらの各行については次のようになる:- 2億深さを超えるネストされた値を持つJSONファイル - 2億行を超えるファイル - 2億文字を超える行

このライブラリは、確実にプロダクションでは使えないね。

一部の開発者が好むシンプルなシングルヘッダーCライブラリ文化について知らないんだね。Tsoding(ストリーマー)は、こういうライブラリを開発・使用するのが好きな典型的な例だよ。彼らは、これらのものが「セキュリティ」や「機能」に焦点を当てていないことを認めていて、それでいいんだ。すべてが何千人もの顧客にさらされる超真剣なビジネスプロジェクトってわけじゃないからね。

diff --git a/sj.h b/sj.h index 60bea9e..25f6438 100644 --- a/sj.h +++ b/sj.h @@ -85,6 +85,7 @@ top: return res; case '{': case '[': + if (r->depth > 999) { r->error = "これ以上深くは行けない"; goto top; } res.type = (*r->cur == '{') ? SJ_OBJECT : SJ_ARRAY; res.depth = ++r->depth; r->cur++; これで直ったよ。

レベルの深さが20億を超えるか、2番目のケースで行数が20億を超えると未定義動作が起きるよ。JSの入力は1GBに制限した方がいい。もしウェブから2GBのJSONファイルを受け取ると、スタックの他の部分で問題が増えるだろうし、2GB以上に対応したいなら、ソース内のすべてのintを64ビットに変更する必要がある。でも、入力が2^64を超えたらまだクラッシュするよ。自分のコードではintのオーバーフローをチェックすることは絶対にしないけどね。

こんなライブラリが安全だとは思わないな。メモリ安全にしたいなら、Fil-Cでコンパイルするべきだよ。

入力の長さをsize_tの代わりにintに変更すればいいんじゃない?技術的には正しい型ではないけど、ユーザーには入力が2^31を超えることはないって明確になると思うよ。

改善のためにPRを出すのもいいね;それは素敵だと思う。

これをパーサーって呼ぶのはちょっと無理がある気がする。普通のレキサーに見えるね。

これは半分パーサーみたいなもので、数字をfloatやintに変換しないから、その上に自分で作らなきゃいけないね。まだ感心してるし使うかもしれないけど、これだけは言っておく。

いいね!次にCでJSONパーサーが必要になったときに見てみるかも。cJSON[0]を何年も使ってて、結構満足してるよ。それ以前はwjelement[1]を使ってたけど、いくつか問題があって結局使わなくなった(理由はもう忘れちゃったけど、かなり前のことだから)。 [0] https://github.com/DaveGamble/cJSON [1] https://github.com/netmail-open/wjelement

これはかなり緩いね。特に問題はないと思うけど(コードを見ずに使う人には注意が必要かも)、これが小さい理由の一つだね。READMEのデモを使うと、{"x",10eee"y"22:5,{[:::,,}]"w"7"h"33 rect: { 10, 22, 7, 33 }

それは間違ってるの?

パーサーは、入力が有効であると仮定しているから、バリデーションはこのライブラリの範囲外の別の問題だよね。データを抽出するだけのライブラリを他に何て呼ぶのか分からないな。

C99では、この構造体がデフォルトで0初期化されるって規定されてるの?それともこの行に「= { 0 }」が抜けてるのかな? - https://github.com/rxi/sj.h/blob/5cb5df45c8c37fd8c2322026a11... - 俺には、r->depthがランダムに初期化されて、sj__discard_untilループの最初のイテレーションで偶然depthと等しくなるように思えるんだけど。

r->depthはここでゼロに初期化されてたはずだよ。 - https://github.com/rxi/sj.h/blob/5cb5df45c8c37fd8c2322026a11...