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/$val records for pointers, arrays for optional and variant). 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_json and from_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::jsonJaiScript
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 defaultif (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 editfree serialize, or save / load, beside the type

Text and values​

nlohmann::jsonJaiScript
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_ubjsonjai::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 pairsthe same
std::optional: converted by the library from version 3.12 (an empty one is null), through an adl_serializer you write before thatbuilt in, as the array [has_value, value]
std::variant through an adl_serializer you writebuilt in, as [index, value]
enums as integers, NLOHMANN_JSON_SERIALIZE_ENUM for namesenums 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_rangejai::serialization_error, a jai::runtime_error

Documents​

nlohmann::jsonJaiScript
json doc = json::parse(text);, doc["hp"][1], doc.contains("gold"), iterationscript: 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 parsingno 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.

C++ (nlohmann::json, non-intrusive)
#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.

C++ (nlohmann::json, intrusive)
#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.

C++ (JaiScript, non-intrusive)
#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.

C++ (JaiScript, intrusive)
#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.

C++ (JaiScript, JAI_PROPERTY)
#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-intrusivenlohmann: intrusiveJaiScript: non-intrusiveJaiScript: intrusiveJaiScript: JAI_PROPERTY
Headersnlohmann/json.hppconvenience.hppplus properties/macros.hpp and property_serialization.hpp
Where the member list livesa macro after the typea macro inside the typea free function after the type, found by ADLa member functionthe declarations themselves
How a member is namedits name in the macroar("hero", save.hero)JAI_NVP(hero)by its declaration
When writing and reading differfree to_json and from_jsonfree save and loadmember save and load
Text out and backjson j = save; j.dump(), json::parse(text).get<SaveGame>()jai::to_json(*eng, save, 0), jai::from_json<SaveGame>(*eng, text)
Key orderalphabeticalthe order of the callsname order
A type with no conversiona compile error at the conversiona compile error where an archive is handed the type; a forgotten friend jai::access; is named in the message
Reflected and visible to scriptsnoyes (dynamic_binder)

Both nlohmann forms write:

text
{"hero":"Alice","items":[{"count":1,"name":"rope"},{"count":3,"name":"torch"}],"level":7,"mood":1}

The JaiScript free and member functions write:

text
{"level": 7,"hero": "Alice","items": [{"name": "rope","count": 1},{"name": "torch","count": 3}],"mood": 1}

The JAI_PROPERTY form writes:

text
{"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. A property_owner copies 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:

jaiscript
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); }
output
{"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 JSON

Nesting 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:

C++
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.