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 covers the same jobs from two sides. On the host side, the archives save and load C++ types (jai::to_json, jai::from_json<T>), in JSON or binary, through the same description that makes a class scriptable. They write their own record format: a root tag, and identity records for shared pointers. On the script side, to_json and from_json turn script values into plain JSON text and back, and a parsed document is an ordinary map you index. What is missing is a C++ DOM type: a document a host wants to walk is a script value. This page maps the library, converts a save type, and shows where each kind of JSON belongs. The archives are documented in Serialization, and From Cereal covers versions, polymorphic types 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)a member template<class Archive> void serialize(Archive& ar) { ar(JAI_NVP(a), JAI_NVP(b)); }, or a free two-parameter serialize(Archive&, T&) found by ADL
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
one macro listing the membersor no function at all: a jai::property_owner whose members are JAI_PROPERTY saves and loads them by name, and the same declaration makes them reflected and script-visible (dynamic_binder)

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 and std::variant through an adl_serializer you writebuilt in: std::optional is [has_value, value], std::variant is [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). A host reaches the builtin through convert_script_function (below)
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​

C++ (nlohmann::json)
#include <nlohmann/json.hpp>

using nlohmann::json;

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)

json j = save;
std::string text = j.dump();
SaveGame back = json::parse(text).get<SaveGame>();
text
{"hero":"Alice","items":[{"count":1,"name":"rope"},{"count":3,"name":"torch"}],"level":7,"mood":1}
C++ (JaiScript)
#include <jaiscript/serialization/convenience.hpp>

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), JAI_NVP(count)); }
};

struct SaveGame {
    int level = 1;
    std::string hero;
    std::vector<Item> items;
    std::optional<int> best;
    Mood mood = Mood::calm;

    template<class Archive>
    void serialize(Archive& ar) { ar(JAI_NVP(level), JAI_NVP(hero), JAI_NVP(items), JAI_NVP(best), JAI_NVP(mood)); }
};

auto eng = jai::engine::make();
std::string text = jai::to_json(*eng, save, 0);
SaveGame back = jai::from_json<SaveGame>(*eng, text);
text
{"level": 7,"hero": "Alice","items": [{"name": "rope","count": 1},{"name": "torch","count": 3}],"best": [true,12],"mood": 1}
  • The macro becomes a function. The member list moves into serialize, one JAI_NVP per member, and the keys follow the call order. Every standard container, std::optional and the smart pointers are built in, with nothing to include.
  • The text is an archive record. A type saved by value writes its properties flat at the root. A root that is not a record (a number, a vector, a map) sits under "value0": jai::to_json(*eng, std::vector<int>{1, 2, 3}, 0) is {"value0": [1,2,3]}. A std::shared_ptr is written as {"$id": 1, "$val": {...}} the first time and {"$id": 1} after, so shared objects load shared, with "$type" naming a derived class (Pointers and identity).
  • The reader is forgiving about order. from_json<T> finds properties by name in any order and skips keys the type does not read, so plain JSON whose keys match loads: {"level": 3, "hero": "Bo", "items": [...], ...}. Optional, variant and pointer members still expect their archive shapes.
  • A missing key is an error. from_json<SaveGame>(*eng, R"({"level": 3})") throws jai::serialization_error: Property 'hero' not found during deserialization (nlohmann throws out_of_range, key 'hero' not found). Malformed text throws too: Expected '"' in JSON.

JSON in script​

Where a host uses json as a loose document (configuration, a web response, tuning data), JaiScript keeps it 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):

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], 2));
output
{"hp":[10,20],"name":"orc","pos":{"x":1.5}}
20 1.5 false
[
  1,
  2.5,
  "three",
  true,
  null
]

A script class instance round-trips with its class name:

jaiscript
class Hero { string name = "Vex"; int hp = 30; }
Hero h;
h.hp = 12;
string text = to_json(h);
Hero copy = from_json(text);
print(text, " -> ", copy.name, " ", copy.hp);
try { from_json("{bad"); } catch (e) { print("caught: ", e); }
output
{"$type":"Hero","$val":{"name":"Vex","hp":12}} -> Vex 12
caught: Expected '"' in JSON

Nesting is bounded by memory, not by a depth limit: the parser and the walks that rebuild script values run on work lists. A host record type whose own serialize/save/load recurses natively stops on save and on load at the host thread's stack, or at JAI_MAX_HOST_RECORD_DEPTH (1024) nested records where the stack cannot be asked, raising 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 wants a document 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. For records the host owns, the archives are the better tool: one description per type, JSON or binary, shared objects kept shared, and the same declaration reflected and scripted.

Where to next​

Serialization documents the archives and script-side JSON in full. From Cereal covers versions, polymorphic types, the JAI_PROPERTY route and converting existing files. The guide's JSON section covers the script builtins.