From nlohmann::json
nlohmann::json gives C++ a JSON value type: parse text into a json, read and write it like a container, and convert your own types through to_json/from_json functions or the NLOHMANN_DEFINE_TYPE_* macros. JaiScript splits those jobs in two:
- Your own records go through the host archives (
jai::to_json,jai::from_json<T>), in JSON or binary, through the same description that makes a class scriptable. The JSON they write is JaiScript's own record format (a"value0"root for a non-record,$id/$type/$valrecords for pointers, arrays foroptionalandvariant). It is a save format, not interchange JSON for an external API. - Plain JSON, the kind a web API or a config file speaks, is the script builtins
to_jsonandfrom_json, which a host can call too (below). A parsed document is a script map you index.
There is no C++ DOM type, json_pointer, patch or SAX interface. A host that builds or validates documents for another service in C++ has reason to keep nlohmann::json for that wire and move only its saved records.
The archives are documented in Serialization; From Cereal covers versions, polymorphic types, the JAI_PROPERTY route and converting existing files.
The concept map
Headers: <jaiscript/serialization/convenience.hpp> for the host entry points; the script builtins come with jai::stdlib::register_all(eng).
Your types
| nlohmann::json | JaiScript |
|---|---|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(T, a, b), NLOHMANN_DEFINE_TYPE_INTRUSIVE(T, a, b) | NON_INTRUSIVE: a free template<class Archive> void serialize(Archive& ar, T& v) { ar("a", v.a); ar("b", v.b); } beside the type, found by ADL. INTRUSIVE: JAI_PROPERTY members of a jai::property_owner, or a member serialize(Archive& ar) { ar(JAI_NVP(a), JAI_NVP(b)); } |
free to_json(json&, const T&) and from_json(const json&, T&) | a member save / load pair, or free two-parameter save(Archive&, const T&) / load(Archive&, T&) |
NLOHMANN_DEFINE_TYPE_*_WITH_DEFAULT, a missing key keeps the member's default | if (ar.has_property("best")) { ar(JAI_NVP(best)); } in a load; a property_owner keeps the initializer of every property the record lacks |
an adl_serializer<T> specialization for a type you cannot edit | free serialize, or save / load, beside the type |
Text and values
| nlohmann::json | JaiScript |
|---|---|
json j = save; std::string text = j.dump();, j.dump(2) | std::string text = jai::to_json(*eng, save, 0);, jai::to_json(*eng, save) (indent 2) |
json::parse(text).get<T>(), j.get_to(value) | T value = jai::from_json<T>(*eng, text); |
json::to_cbor, to_msgpack, to_bson, to_ubjson | jai::to_binary(*eng, value) and jai::from_binary<T>(*eng, bytes), the archives' own compact format (little-endian everywhere), or to_base64 / from_base64; not an interchange format |
std::map<std::string, T> as an object, other maps as arrays of pairs | the same |
std::optional: converted by the library from version 3.12 (an empty one is null), through an adl_serializer you write before that | built in, as the array [has_value, value] |
std::variant through an adl_serializer you write | built in, as [index, value] |
enums as integers, NLOHMANN_JSON_SERIALIZE_ENUM for names | enums as integers; a name is a string the type's serialize converts |
keys in alphabetical order (json), or insertion order (ordered_json) | the order of the ar(...) calls; a property_owner writes each level's properties in name order. A reader finds keys by name in any order |
json::parse_error, json::type_error, json::out_of_range | jai::serialization_error, a jai::runtime_error |
Documents
| nlohmann::json | JaiScript |
|---|---|
json doc = json::parse(text);, doc["hp"][1], doc.contains("gold"), iteration | script: var doc = from_json(text); is a map of arrays and maps: doc["hp"][1], doc.contains("gold"), for (auto [k, v] : doc) |
building a value: json j; j["name"] = "orc"; | a map or array literal in script, {"name": "orc"}; eng->make_map() and eng->make_array() on the host |
doc.dump() | to_json(doc), to_json(doc, 2): plain JSON with keys in order |
json_pointer, patch, merge_patch, diff, SAX parsing | no counterpart |
A save type converted
One save type, five ways. The macro and function forms keep every type and member at the same line number;
JAI_PROPERTY is shorter.
A macro after each type lists its members.
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
enum class Mood { calm = 0, angry = 1 };
struct Item {
std::string name;
int count = 1;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Item,
name,
count
)
struct SaveGame {
int level = 1;
std::string hero;
std::vector<Item> items;
Mood mood = Mood::calm;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SaveGame,
level,
hero,
items,
mood
)
int main() {
Item rope;
rope.name = "rope";
Item torch;
torch.name = "torch";
torch.count = 3;
SaveGame save;
save.level = 7;
save.hero = "Alice";
save.items = {rope, torch};
save.mood = Mood::angry;
nlohmann::json j = save;
std::string text = j.dump();
SaveGame back = nlohmann::json::parse(text).get<SaveGame>();
}The macro sits inside each type.
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
enum class Mood { calm = 0, angry = 1 };
struct Item {
std::string name;
int count = 1;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Item,
name,
count
)
};
struct SaveGame {
int level = 1;
std::string hero;
std::vector<Item> items;
Mood mood = Mood::calm;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(SaveGame,
level,
hero,
items,
mood
)
};
int main() {
Item rope;
rope.name = "rope";
Item torch;
torch.name = "torch";
torch.count = 3;
SaveGame save;
save.level = 7;
save.hero = "Alice";
save.items = {rope, torch};
save.mood = Mood::angry;
nlohmann::json j = save;
std::string text = j.dump();
SaveGame back = nlohmann::json::parse(text).get<SaveGame>();
}A free serialize replaces each non-intrusive macro.
#include <jaiscript/serialization/convenience.hpp>
#include <string>
#include <vector>
enum class Mood { calm = 0, angry = 1 };
struct Item {
std::string name;
int count = 1;
};
template<class Archive> void serialize(Archive& ar, Item& item) {
ar("name", item.name);
ar("count", item.count);
}
struct SaveGame {
int level = 1;
std::string hero;
std::vector<Item> items;
Mood mood = Mood::calm;
};
template<class Archive> void serialize(Archive& ar, SaveGame& save) {
ar("level", save.level);
ar("hero", save.hero);
ar("items", save.items);
ar("mood", save.mood);
}
int main() {
Item rope;
rope.name = "rope";
Item torch;
torch.name = "torch";
torch.count = 3;
SaveGame save;
save.level = 7;
save.hero = "Alice";
save.items = {rope, torch};
save.mood = Mood::angry;
auto eng = jai::engine::make();
std::string text = jai::to_json(*eng, save, 0);
SaveGame back = jai::from_json<SaveGame>(*eng, text);
}A member serialize replaces each intrusive macro.
#include <jaiscript/serialization/convenience.hpp>
#include <string>
#include <vector>
enum class Mood { calm = 0, angry = 1 };
struct Item {
std::string name;
int count = 1;
template<class Archive> void serialize(Archive& ar) {
ar(JAI_NVP(name));
ar(JAI_NVP(count));
}
};
struct SaveGame {
int level = 1;
std::string hero;
std::vector<Item> items;
Mood mood = Mood::calm;
template<class Archive> void serialize(Archive& ar) {
ar(JAI_NVP(level));
ar(JAI_NVP(hero));
ar(JAI_NVP(items));
ar(JAI_NVP(mood));
}
};
int main() {
Item rope;
rope.name = "rope";
Item torch;
torch.name = "torch";
torch.count = 3;
SaveGame save;
save.level = 7;
save.hero = "Alice";
save.items = {rope, torch};
save.mood = Mood::angry;
auto eng = jai::engine::make();
std::string text = jai::to_json(*eng, save, 0);
SaveGame back = jai::from_json<SaveGame>(*eng, text);
}Each type derives jai::property_owner and declares its members with JAI_PROPERTY; no function is written.
#include <jaiscript/properties/macros.hpp>
#include <jaiscript/properties/property_serialization.hpp>
#include <jaiscript/serialization/convenience.hpp>
#include <string>
#include <vector>
enum class Mood { calm = 0, angry = 1 };
struct Item : jai::property_owner<Item> {
JAI_PROPERTY((std::string), name);
JAI_PROPERTY((int), count, 1);
};
struct SaveGame : jai::property_owner<SaveGame> {
JAI_PROPERTY((int), level, 1);
JAI_PROPERTY((std::string), hero);
JAI_PROPERTY((std::vector<Item>), items);
JAI_PROPERTY((Mood), mood, Mood::calm);
};
int main() {
Item rope;
rope.name = "rope";
Item torch;
torch.name = "torch";
torch.count = 3;
SaveGame save;
save.level = 7;
save.hero = "Alice";
save.items = {rope, torch};
save.mood = Mood::angry;
auto eng = jai::engine::make();
std::string text = jai::to_json(*eng, save, 0);
SaveGame back = jai::from_json<SaveGame>(*eng, text);
}| nlohmann: non-intrusive | nlohmann: intrusive | JaiScript: non-intrusive | JaiScript: intrusive | JaiScript: JAI_PROPERTY | |
|---|---|---|---|---|---|
| Headers | nlohmann/json.hpp | convenience.hpp | plus properties/macros.hpp and property_serialization.hpp | ||
| Where the member list lives | a macro after the type | a macro inside the type | a free function after the type, found by ADL | a member function | the declarations themselves |
| How a member is named | its name in the macro | ar("hero", save.hero) | JAI_NVP(hero) | by its declaration | |
| When writing and reading differ | free to_json and from_json | free save and load | member save and load | ||
| Text out and back | json j = save; j.dump(), json::parse(text).get<SaveGame>() | jai::to_json(*eng, save, 0), jai::from_json<SaveGame>(*eng, text) | |||
| Key order | alphabetical | the order of the calls | name order | ||
| A type with no conversion | a compile error at the conversion | a compile error where an archive is handed the type; a forgotten friend jai::access; is named in the message | |||
| Reflected and visible to scripts | no | yes (dynamic_binder) | |||
Both nlohmann forms write:
{"hero":"Alice","items":[{"count":1,"name":"rope"},{"count":3,"name":"torch"}],"level":7,"mood":1}The JaiScript free and member functions write:
{"level": 7,"hero": "Alice","items": [{"name": "rope","count": 1},{"name": "torch","count": 3}],"mood": 1}The JAI_PROPERTY form writes:
{"hero": "Alice","items": [{"count": 1,"name": "rope"},{"count": 3,"name": "torch"}],"level": 7,"mood": 1}A missing key: loading {"level": 3}, nlohmann throws out_of_range
(key 'hero' not found) unless the type uses a _WITH_DEFAULT macro; the JaiScript functions
throw jai::serialization_error (Property 'hero' not found during deserialization) unless a
load reads the key under if (ar.has_property("hero")); the JAI_PROPERTY form
gives level 3 and every other property its initializer.
- A property reads as the member did:
save.level = 7;,save.hero.get()where an expression needs the plain value,save.items->push_back(Item{})to reach the vector. Aproperty_ownercopies and moves as the struct it replaces does. - The text is an archive record. A root that is not a record (a number, a vector, a map, an owning pointer) sits under
"value0":jai::to_json(*eng, std::vector<int>{1, 2, 3}, 0)is{"value0": [1,2,3]}. - The reader finds properties by name in any order and skips keys the type does not read; optional, variant and pointer members still expect their archive shapes. Malformed text throws
jai::serialization_error:Expected '"' in JSON.
JSON in script
from_json parses text into maps, arrays, numbers, strings, bool and null; to_json writes any script value back as plain JSON, with map keys in order so the same data always writes the same text (JSON in the guide). A script class instance round-trips with its class name:
var save = {"name": "orc", "hp": [10, 20], "pos": {"x": 1.5}};
string text = to_json(save);
var back = from_json(text);
print(text);
print(back["hp"][1], " ", back["pos"]["x"], " ", back.contains("gold"));
print(to_json([1, 2.5, "three", true, null]));
class Hero { string name = "Vex"; int hp = 30; }
Hero h;
h.hp = 12;
Hero copy = from_json(to_json(h));
print(to_json(h), " -> ", copy.name, " ", copy.hp);
try { from_json("{bad"); } catch (e) { print("caught: ", e); }{"hp":[10,20],"name":"orc","pos":{"x":1.5}}
20 1.5 false
[1,2.5,"three",true,null]
{"$type":"Hero","$val":{"name":"Vex","hp":12}} -> Vex 12
caught: Expected '"' in JSONNesting is bounded by memory, not by a depth limit. A host record type whose own serialize recurses natively stops at the host thread's stack, or at JAI_MAX_HOST_RECORD_DEPTH (1024) nested records where the stack cannot be asked, with Record nesting depth exceeds the host thread's stack or Maximum record nesting depth (1024) exceeded.
JSON on the host side
The script builtins are functions like any other, so a host that needs plain JSON in C++ calls them through convert_script_function and works with the jai::script_value it gets back:
auto eng = jai::engine::make();
jai::stdlib::register_all(eng);
auto parse = jai::convert_script_function<jai::script_value(const std::string&)>(eng->get_variable("from_json"));
auto dump = jai::convert_script_function<std::string(const jai::script_value&)>(eng->get_variable("to_json"));
jai::script_value doc = parse(R"({"name": "orc", "hp": [10, 20], "tag": null})");
std::string name = doc.as<std::map<std::string, jai::script_value>>().at("name").as_string(); // "orc"
std::string text = dump(doc); // {"hp":[10,20],"name":"orc","tag":null}A document a script will use goes in as a global (eng->add_global("config", doc)), and a script can hand one back the same way.