{"record":{"id":"a3e042e30ff98d2e","repo":"amark/gun","slug":"data-at-root-of-graph-must-be-a-node-an-object","errorCode":null,"errorMessage":"Data at root of graph must be a node (an object).","messagePattern":"Data at root of graph must be a node \\(an object\\)\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/put.js","lineNumber":38,"sourceCode":"\tas.ran = as.ran || ran;\n\t//var path = []; as.via.back(at => { at.get && path.push(at.get.slice(0,9)) }); path = path.reverse().join('.');\n\t// TODO: Perf! We only need to stun chains that are being modified, not necessarily written to.\n\t(function walk(){\n\t\tvar to = as.todo, at = to.pop(), d = at.it, cid = at.ref && at.ref._.id, v, k, cat, tmp, g;\n\t\tstun(as, at.ref);\n\t\tif(tmp = at.todo){\n\t\t\tk = tmp.pop(); d = d[k];\n\t\t\tif(tmp.length){ to.push(at) }\n\t\t}\n\t\tk && (to.path || (to.path = [])).push(k);\n\t\tif(!(v = valid(d)) && !(g = Gun.is(d))){\n\t\t\tif(!Object.plain(d)){ ran.err(as, \"Invalid data: \"+ check(d) +\" at \" + (as.via.back(function(at){at.get && tmp.push(at.get)}, tmp = []) || tmp.join('.'))+'.'+(to.path||[]).join('.')); return }\n\t\t\tvar seen = as.seen || (as.seen = []), i = seen.length;\n\t\t\twhile(i--){ if(d === (tmp = seen[i]).it){ v = d = tmp.link; break } }\n\t\t}\n\t\tif(k && v){ at.node = state_ify(at.node, k, s, d) } // handle soul later.\n\t\telse {\n\t\t\tif(!as.seen){ ran.err(as, \"Data at root of graph must be a node (an object).\"); return }\n\t\t\tas.seen.push(cat = {it: d, link: {}, todo: g? [] : Object.keys(d).sort().reverse(), path: (to.path||[]).slice(), up: at}); // Any perf reasons to CPU schedule this .keys( ?\n\t\t\tat.node = state_ify(at.node, k, s, cat.link);\n\t\t\t!g && cat.todo.length && to.push(cat);\n\t\t\t// ---------------\n\t\t\tvar id = as.seen.length;\n\t\t\t(as.wait || (as.wait = {}))[id] = '';\n\t\t\ttmp = (cat.ref = (g? d : k? at.ref.get(k) : at.ref))._;\n\t\t\t(tmp = (d && (d._||'')['#']) || tmp.soul || tmp.link)? resolve({soul: tmp}) : cat.ref.get(resolve, {run: as.run, /*hatch: 0,*/ v2020:1, out:{get:{'.':' '}}}); // TODO: BUG! This should be resolve ONLY soul to prevent full data from being loaded. // Fixed now?\n\t\t\t//setTimeout(function(){ if(F){ return } console.log(\"I HAVE NOT BEEN CALLED!\", path, id, cat.ref._.id, k) }, 9000); var F; // MAKE SURE TO ADD F = 1 below!\n\t\t\tfunction resolve(msg, eve){\n\t\t\t\tvar end = cat.link['#'];\n\t\t\t\tif(eve){ eve.off(); eve.rid(msg) } // TODO: Too early! Check all peers ack not found.\n\t\t\t\t// TODO: BUG maybe? Make sure this does not pick up a link change wipe, that it uses the changign link instead.\n\t\t\t\tvar soul = end || msg.soul || (tmp = (msg.$$||msg.$)._||'').soul || tmp.link || ((tmp = tmp.put||'')._||'')['#'] || tmp['#'] || (((tmp = msg.put||'') && msg.$$)? tmp['#'] : (tmp['=']||tmp[':']||'')['#']);\n\t\t\t\t!end && stun(as, msg.$);\n\t\t\t\tif(!soul && !at.link['#']){ // check soul link above us\n\t\t\t\t\t(at.wait || (at.wait = [])).push(function(){ resolve(msg, eve) }) // wait\n\t\t\t\t\treturn;","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/amark/gun/blob/552227599d47ba0493824b7fcf8a00c8cd6404ba/src/put.js#L20-L56","documentation":"Gun's put algorithm requires the root of the written data to be a graph node (a plain object). When the top-level value being put is a primitive (string, number, boolean, null) and there is no enclosing seen context (as.seen is undefined, i.e. this is the root of the put walk), the write is rejected: 'Data at root of graph must be a node (an object).'","triggerScenarios":"Calling gun.put('string'), gun.put(42), gun.put(true), gun.put(null) — a primitive at the root of a put; also gun.get(soul).put(primitive) where the walk starts at root without a key context, hitting the else branch with !as.seen.","commonSituations":"Migrating from key-value stores and putting bare values instead of node objects; writing a single field without wrapping it in an object; refactored code that lost the object wrapper; passing the result of a computation that unexpectedly became a primitive (or undefined/null).","solutions":["Wrap the value in a node object: gun.get('soul').put({ field: value }) instead of putting the bare value","Check the value at the call site — if it can be undefined/null/primitive, build the object explicitly","For dynamic payloads, verify typeof value === 'object' before put and handle primitives separately","Put the value under a key inside an existing node rather than at graph root"],"exampleFix":"// before\ngun.get('settings').put('dark-mode');\n// after\ngun.get('settings').put({ theme: 'dark-mode' });","handlingStrategy":"validation","validationCode":"function assertRootNode(value) {\n  if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n    throw new Error('gun.put root must be a plain object node');\n  }\n}\nassertRootNode(payload);","typeGuard":"const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;","tryCatchPattern":"gun.get('soul').put(payload, ack => {\n  if (ack && String(ack.err).includes('root of graph')) {\n    console.error('payload must be an object node, got:', typeof payload);\n    return;\n  }\n});","preventionTips":["Always put node objects, never bare primitives","Wrap single values under a key: put({ field: value })","Guard dynamic payloads: check typeof payload === 'object' before put","Watch for variables that may be undefined/null at call time"],"tags":["gun","put","validation","root-node"],"backgroundTag":"invalid-graph-data","analyzedSha":"552227599d47ba0493824b7fcf8a00c8cd6404ba","analyzedAt":"2026-09-02T18:40:58.370Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T21:17:11.164Z"}