Skip to content

Editing a document

A json_document/json_view lets you look at a parsed text without copying it, but not change it. basic_json_document<BasicJsonType, view_mode::editable> — more conveniently spelled json_editable_document or ordered_json_editable_document — also lets you set a value, push_back onto or insert into an array, and erase an object member or an array element, still without ever building a basic_json tree for parts you do not touch.

The mode defaults to view_mode::read_only, so json_document/ordered_json_document are unaffected — they carry none of the bookkeeping edits need, and calling set/push_back/insert/erase on one is a compile error, not a runtime one.

Why: editing without reformatting

A price like 19.90 is exactly the kind of value that makes editing a json or ordered_json value in place lossy. Say you parse a configuration file, patch one field, and write it back:

  • json re-sorts every key on the way in (object_t is a std::map) and rewrites every number to its shortest round-trip form on the way out — a one-field patch turns into a diff that reorders the whole file and rewrites 19.90 to 19.9.
  • ordered_json keeps the key order, but still rewrites every number the same way: parsing has already reduced it to a double/int64_t, and there is no way back to how it was spelled in the source text.

The patch of the example below (set "host", add "retries") written back by each of them:

json

keys sorted, numbers rewritten

{"host":"db2",
 "name":"cache",
 "port":6379,
 "price":19.9,
 "retries":3}

ordered_json

key order kept, numbers rewritten

{"name":"cache",
 "host":"db2",
 "port":6379,
 "price":19.9,
 "retries":3}

json_editable_document

order and spelling kept

{"name":"cache",
 "host":"db2",
 "port":6379,
 "price":19.90,
 "retries":3}

An editable document keeps both. dump() of an edited document writes members in document order — a member set added goes at the end, exactly where it was inserted, and an erased member simply leaves a gap: everything around it keeps its place — and number_format::source keeps the exact spelling of every number an edit did not itself touch; a number an edit did touch is written the way BasicJsonType::dump() would write it, since there is no source spelling for a brand new value. See Writing a view back.

Example: patch a configuration, keeping member order and number spellings
#include <iostream>
#include <nlohmann/json_view.hpp>

using nlohmann::json;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;

int main()
{
    // a configuration file, as it might be read from disk
    const std::string text = R"({"name": "cache", "host": "db1", "port": 6379, "price": 19.90})";
    std::cout << text << "\n\n";

    // patch two fields -- "price" is never touched
    json_editable_document doc = json_editable_document::parse(text);
    doc.set(doc.root(), "host", "db2");
    doc.set(doc.root(), "retries", 3);

    // member order (the new member at the end) and the untouched number's
    // exact spelling survive
    std::cout << doc.root().dump(-1, ' ', false, json_editable_view::number_format::source) << '\n';

    // the same patch on a plain json value: keys are sorted (object_t is a
    // std::map), and "price" is rewritten even though the patch never
    // touched it
    json plain = json::parse(text);
    plain["host"] = "db2";
    plain["retries"] = 3;
    std::cout << plain.dump() << '\n';
}

Output:

{"name": "cache", "host": "db1", "port": 6379, "price": 19.90}

{"name":"cache","host":"db2","port":6379,"price":19.90,"retries":3}
{"host":"db2","name":"cache","port":6379,"price":19.9,"retries":3}

What stays valid, and what an edit costs

The source text itself is never written, and the parsed index never moves — a value keeps the node it was parsed into for as long as it is not itself replaced. So every view taken before an edit, including a previously obtained root(), stays valid and, if it still refers to the edited value, sees the edit; a view of a value a later edit drops or replaces just keeps showing what it last held. New values go to storage the document allocates and owns on demand. The one thing an edit does invalidate is the iterators taken over an edited array or object: the first time an element is added to it, inserted into it, erased from it, or replaced by a value that spans a different number of nodes, its elements move from the parsed, fixed layout to a growable block of links so that push_back can later grow it in amortized constant time — existing elements are not touched, but an iterator that was walking the old layout no longer matches. A string obtained with get_string() is unaffected either way and stays valid across further edits. See basic_json_document's Edits for the details, and set's Exception safety for what an edit guarantees if it throws (the basic guarantee, not the strong one dump() and the read-only functions provide). How edits are kept in the index is described in the architecture overview.

Step through the edits of a small document (the node fields follow the architecture overview):

Step 1 of 5: parsed After parse: nine nodes, nothing owned yet. The dump with number_format::source is {"port":6379,"price":19.90,"tags":["eu","a\"b"]}. parsed index: fixed, never moves the source text is never written 0 object { } 3 1 key port 2 uint 6379 3 key price 4 float 19.90 5 key tags 6 array [ ] 2 7 string "eu" 8 string a"b owned storage: allocated on demand, never moves empty: a read-only json_document has none of this view v = root()["tags"] (taken after parse): valid, sees every edit iterator over tags (taken after parse): valid dump(): {"port":6379,"price":19.90,"tags":["eu","a\"b"]}
Step 1 of 5. After parsing: nine nodes in the parsed index, no owned storage yet.
Step 2 of 5: doc.set(doc.root(), "port", 6380) set: the node of the integer is overwritten in place. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["eu","a\"b"]}. parsed index: fixed, never moves the source text is never written 0 object { } 3 1 key port 2 uint 6380 3 key price 4 float 19.90 5 key tags 6 array [ ] 2 7 string "eu" 8 string a"b owned storage: allocated on demand, never moves empty: a read-only json_document has none of this view v = root()["tags"] (taken after parse): valid, sees every edit iterator over tags (taken after parse): valid dump(): {"port":6380,"price":19.90,"tags":["eu","a\"b"]}
Step 2 of 5. doc.set(doc.root(), "port", 6380) overwrites the node of the integer in place; no member is added or removed, so the object keeps its parsed layout.
Step 3 of 5: doc.erase(doc.root()["tags"], 0) erase: the array moves to a block of links in owned storage. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b"]}. parsed index: fixed, never moves the source text is never written 0 object { } 3 1 key port 2 uint 6380 3 key price 4 float 19.90 5 key tags 6 array moved [ ] 2 7 string "eu" unreferenced 8 string a"b owned storage: allocated on demand, never moves 0' header [ ] 1 1' link → 8 free view v = root()["tags"] (taken after parse): valid, sees every edit iterator over tags (taken after parse): invalid: tags no longer uses the parsed layout dump(): {"port":6380,"price":19.90,"tags":["a\"b"]}
Step 3 of 5. doc.erase(doc.root()["tags"], 0): the array is edited for the first time, so its elements move to a block of links in owned storage. The node of "eu" stays in the index, but nothing refers to it any more; iterators over the array taken before are invalid.
Step 4 of 5: doc.push_back(doc.root()["tags"], "new") push_back: a new node, and a link appended to the block. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b","new"]}. parsed index: fixed, never moves the source text is never written 0 object { } 3 1 key port 2 uint 6380 3 key price 4 float 19.90 5 key tags 6 array moved [ ] 2 7 string "eu" unreferenced 8 string a"b owned storage: allocated on demand, never moves 0' header [ ] 2 1' link → 8 2' link → 3' 3' string "new" edited text new view v = root()["tags"] (taken after parse): valid, sees every edit iterator over tags (taken after parse): invalid: tags no longer uses the parsed layout dump(): {"port":6380,"price":19.90,"tags":["a\"b","new"]}
Step 4 of 5. doc.push_back(doc.root()["tags"], "new"): the new string gets its own node, its text goes to the buffer of edited text, and a link to it is appended to the block of the array.
Step 5 of 5: doc.set(doc.root(), "retries", 3) set of a new member: the object moves to a block as well. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b","new"],"retries":3}. parsed index: fixed, never moves the source text is never written 0 object moved { } 3 1 key port 2 uint 6380 3 key price 4 float 19.90 5 key tags 6 array moved [ ] 2 7 string "eu" unreferenced 8 string a"b owned storage: allocated on demand, never moves 0' header [ ] 2 1' link → 8 2' link → 3' 3' string "new" 4' uint 3 5' header { } 4 6' key port 7' link → 2 8' key price 9' link → 4 10' key tags 11' link → 6 12' key retries 13' link → 4' edited text newretries view v = root()["tags"] (taken after parse): valid, sees every edit iterator over tags (taken after parse): invalid: tags no longer uses the parsed layout dump(): {"port":6380,"price":19.90,"tags":["a\"b","new"],"retries":3}
Step 5 of 5. doc.set(doc.root(), "retries", 3): a new member, so the object moves to a block too: copies of its keys, links to the values it already had, then the new key and a link to the new integer.

The node index and the owned storage of the document after each of five edits. Use Back, Next, or the numbered buttons to switch between the steps; without JavaScript all steps are shown one below the other.

See also

Version history

  • Added in version 3.13.0 unreleased.