C++でjsonのschemaを使ったvalidationをする
「神戸電子専門学校 ゲーム技研部 Advent Calendar 2023」6日目の記事です。
はじめに
C++でnlohmann_json
を使ってjsonをロードし、要素にアクセスする際、このようなエラーに遭遇したことがあると思います。
そしてこのエラーが何なのか、Visual Studioの出力をみてもわかりません。つまりjsonを変更した部分を戻しながらデバッグしなければならないのです。私のC++でのゲーム開発ではこれに結構悩まされていました。それを回避するために今回はjson-schema-validator
というものを導入しました。
予備知識
みなさんはC++でjsonを使っているでしょうか?C#(System.Text.JsonやJsonUtility(Unity))やPython(json)なら標準ライブラリでパーサーが搭載されているので使ったこともあるのではないかと思いますが、C++で利用するには外部ライブラリを導入するしかありません。代表的なjsonのライブラリはnlohmann_json
やrapidjson
などがあります。
jsonとは
そもそもjsonとはなんなのかを少しだけ説明します。よく見かけるのは、アプリケーションの設定ファイルで'*.json'として使われたり、あとはAPI通信でのデータのやり取りで使われたりしています。
{
"shirokuma1101": {
"income": 99999999,
"skill": "nai"
}
}
このようなフォーマットで、文字列や数値を辞書形式、配列形式でテキストファイルとして保存できます。
シリアライズ化とは
少し話は変わりますが、シリアライズ化はしているでしょうか?そしてそもそもシリアライズとはなんでしょうか?シリアライズとは、プロセス(プログラム)中の変数をそのプロセス(プログラム)以外でも参照できるようにすることを指します。(ちゃんと説明するとちょっと違いますが、、、)
具体的な例を挙げると、前述したjsonの使用例がそれにあたります。要約すると、変数などのプログラム内でしか利用できないデータをテキストなどに保存することをシリアライズ化、そして、逆にそのテキストデータなどからプログラム内で利用できるようにすることをデシリアライズ化と言います。
そしてゲーム開発においてはシリアライズ/デシリアライズを頻繁に使用します。例えばアクションゲームのプレイヤーのパラメーターだけでも、攻撃力、防御力、歩行速度、走る速度、体力等々、、、結構な量のパラメーターが存在します。そして、パラメータのシリアライズ化をしていないと、例えばプレイヤーの攻撃力を増やしたい場合に
void Init() {
- m_playerAttackDamage = 10;
+ m_playerAttackDamage = 100;
}
とソースコードを変更し、変更を適用するために毎回コンパイルする必要があります。これでは値の調整に時間がかかってしまいます。これを回避するには、この数値を動的に外部ファイルから読み込むようにすればプログラムはコンパイルせず外部ファイルでの数値の変更だけで値の変更が適用されるようになります。
void Init() {
m_settingFile = json::load("settings.json");
m_playerAttackDamage = m_settingFile["player_attack_damage"];
}
このように、変数にアクセスできる処理を書いておくだけで済むようになりました。そして、このm_settingFile
がデシリアライズされたデータです。
ではこのシリアライズ化したデータはどのように保存するのがいいでしょうか。今回は話の本筋でもあるjsonを利用します。前述したプログラムの場合、
{
"player_attack_damage": 100
}
と記述すればOKになります。これでソースファイルを直接触らなくても、パラメーター調整ができるようになりました。そして私の学校でも一時期jsonによるシリアライズ化が流行っていました。(多分私のせいです)
ちなみにUnityではYAMLというものを利用しています。
Schemaとは
jsonに限らない話ですが、schema(スキーマ)という概念が存在します。これは、「このschemaに従って書いてね」みたいな仕様書のようなものです。schemaをきちんと書いていると、一貫性があるjsonを書くことができ、またvalidationをすることで問題個所を特定しやすくできます。そしてschemaはjson形式で書きます。
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"schema": {
"type": "string",
"additionalProperties": false
},
"transform": {
"type": "object",
"additionalProperties": false,
"properties": {
"fixed": {
"type": "boolean",
"additionalProperties": false
},
"position": {
"type": "array",
"additionalProperties": false,
"minItems": 3,
"maxItems": 3,
"items": [
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
}
]
}
},
"required": [
"fixed"
]
}
},
"required": [
"transform"
]
}
schemaの一例です。ゲーム開発にちなんで、UnityのTransformのようなschemaを書いてみました。要所を説明すると
-
"type"
型の指定。 -
"additionalProperties"
指定された以外の値の設定を許容するかどうか。 -
"required"
同じ階層にあるキーを必須にするか。
です。このschemaの場合、transform
とfixed
が必須として指定されているので最小構成は
{
"transform": {
"fixed": false
}
}
となります。
jsonのインストール
今回はWindows&Visual Studio2022という環境で行っていきます。
前述したとおり、C++には標準でjsonのパーサーはないので、外部ライブラリであるnlohmann_json
を導入します。
こちらのリポジトリURLの右側のReleases
にある最終リリースをダウンロードします。jsonを利用するだけなら、Assets
の中にあるinclude.zip
で良いのですが、あとで他のファイルも使うので今回はSource code
をダウンロードします。
今回は適当にプロジェクトフォルダの中にLibrary
というフォルダを作り、その中にzipを展開したフォルダを入れます。そして最後にVisual Studio側でパスの設定を行います。
画面上部のプロジェクト
の一番下の{プロジェクト名}のプロパティ
をクリックします。そして、C/C++
→全般
を選択し、右に表示された表の一番上にある追加のインクルードディレクトリ
を選択し、右の欄にある下矢印のアイコンをクリックし<編集...>
をクリックします。すると新しく追加のインクルードディレクトリ
というウィンドウが表示されます。ここにinclude
へのパス(私の環境では.\Library\json-3.11.3\include
)と入力したらOK
を押して画面を閉じます。
実行確認
実際に導入できているかどうか試してみます。以下のコードを入力してコンパイルしてみてください。
#include "nlohmann/json.hpp"
int main()
{
using Json = nlohmann::json;
}
コンパイルエラーが起きていなければ導入完了です。
次にjsonのロードもできるかどうか試してみます。プロジェクトフォルダにAssets
というフォルダを作成し、その中にtest.json
を作成します。そしてjsonの中身を適当に書いていきます。
{
"suuzi": 10,
"moziretu": "mozimozi"
}
次にプログラム側で取得できるかどうか確認します。以下のコードを入力してください。
#include <fstream>
#include <iostream>
#include "nlohmann/json.hpp"
int main()
{
using Json = nlohmann::json;
Json j;
std::ifstream ifs("Assets/test.json");
if (ifs) {
ifs >> j;
}
auto suuzi = j["suuzi"];
auto moziretu = j["moziretu"];
std::cout << suuzi << std::endl;
std::cout << moziretu << std::endl;
}
実行したらコンソールにこのように表示されていれば正常です。
json-schema-validatorのインストール
次にjson-schema-validatorをインストールします。こちらはライブラリをコンパイルする必要があるので少し手順が複雑です。
(そして今後アップデートによって手順が変更されるかもしれません。)
まず、CMakeが必要なのでCMakeをインストールします。インストール手順は割愛しますが、コマンドプロンプト等でcmake --version
が実行できCMake-guiが使える状態であればOKです。
こちらのリポジトリURLの右側にあるReleases
にある最終リリースのSource code
をダウンロードし、Library
フォルダに展開します。次に展開したフォルダの中にbuild
というフォルダを作成します。
次に、Cmake-guiを実行して、Where is the source code:
にはプロジェクトのパスを、Where to build the vinaries:
には作成したbuild
ディレクトリを指定します。
左下のConfigure
をクリックすると、オプションを選択できます。私は、Visual Studio 17 2022
とx64
を指定しました。(後者に関しては、指定しなくてもx64
になるようです。)
Finish
をクリックすると、エラーが起きました。これは依存関係であるnlohmann_json
のパスが見つからなかったため表示されています。
次にnlohmann_json
を指定できるようにするための準備を行います。nlohmann_json
の方で同じくbuild
フォルダを作成します。コマンドプロンプト等でカレントディレクトリがbuild
になっている状態でcmake ..
を実行します。これでnlohmann_json
側の準備は完了です。
先ほど作成したnlohmann_json
側のbuild
のパスをnlohmann_json_DIR
に設定します。
これでConfigure
をクリックするとエラーなくコンソールにConfigure done
と表示されました。そして最後にGenerate
をクリックします。すると、json_schema_validator
側のbuild
フォルダにファイルやフォルダが生成できているのが確認できると思います。ビルドするにはその中のnlohmann_json_schema_validator.sln
を開きます。
プロジェクトが開くので、Ctrl+B
でビルドを開始します。エラーが出ていなければ成功です。
(Release
ビルドのライブラリが欲しい場合は、画面上部からRelease
を選択してビルドしてください。)
ビルドの成果物はbuild/Debug'or
build/Release'に保存されています。今回はその中のnlohmann_json_schema_validator.lib
を使います。
ライブラリのコンパイルは以上です。
プロジェクトの方へ戻ります。ライブラリへのパスを設定するために、プロパティを開きます。C/C++
→全般
の追加のインクルードディレクトリ
にsrc
へのパス(私の環境では.\Library\json-schema-validator-2.2.0\src
)を追加します。次に、リンカー
→全般
の追加のライブラリディレクトリ
にDebug
へのパス(私の環境では.\Library\json-schema-validator-2.2.0\build\Debug
)を追加します。これでパスの設定は終わりです。
実行確認
こちらも実際に導入できているかどうか試してみます。以下のコードを入力してコンパイルしてみてください。
#include "nlohmann/json-schema.hpp"
#pragma comment(lib, "nlohmann_json_schema_validator.lib")
int main()
{
using JsonValidator = nlohmann::json_schema::json_validator;
}
コンパイルエラーが起きていなければ導入完了です。
使い方
ここからは実際にValidatorというものを使ってみます。Assets
フォルダにschema.json
を作成します。このファイルにはSchemaとは
で紹介したschemaをそのまま書いてみます。そしてtest.json
も紹介した最小構成に書き換えます。
#include <fstream>
#include <iostream>
#include "nlohmann/json.hpp"
#include "nlohmann/json-schema.hpp"
#pragma comment(lib, "nlohmann_json_schema_validator.lib")
int main()
{
using Json = nlohmann::json;
using JsonValidator = nlohmann::json_schema::json_validator;
Json j;
std::ifstream ifs("Assets/test.json");
if (ifs) {
ifs >> j;
}
JsonValidator validator;
Json schema;
std::ifstream ifs2("Assets/schema.json");
if (ifs2) {
ifs2 >> schema;
}
try {
validator.set_root_schema(schema);
}
catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
try {
validator.validate(j);
}
catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
auto fixed = j["transform"]["fixed"];
std::cout << fixed << std::endl;
}
プログラム全体です。これで実行するとfalse
と表示されるだけでおわります。validatorの威力はここからです。test.json
の"fixed": false
を消してみます。
{
"transform": {
}
}
これで実行すると、std::cout << e.what() << std::endl;
の部分で何が不足しているかを具体的に表示してくれるようになりました。今回はschemaでfixed
を必須(required)としているのに、読み込んだjsonでは存在しないため、このようなエラー内容になっています。
schemaで何を指定できるかはこちらの公式リファレンスに書いてあります。
おわりに
2023/12/06 一部文章を修正
実はrapidjson
にはvalidation機能がデフォルトで入っているのですが、nlohmann_json
のほうが好きなのでこちらで紹介してみました。schemaの構文は共通なのでどちらでも利用できると思います。
余談ですが、就活中にお話ししたゲーム会社の方で、「マジックナンバー絶対許さないマン」もいたので、そういう意味でも外部ファイル化によるシリアライズ化はしておいたほうが良いと思います。
Discussion